追查「關於 delete_all 的 Bug」

2016-03-12 23:50

has_many 沒有指定 dependent 的時候,使用 object.associations.delete_all,其行為可能會讓你有點意外

Pic CC-BY by Guilherme Tavares https://flic.kr/p/3AvBtV

不經意看到這篇文章 關於delete_all的Bug,把他的 git repo 抓下來試試看,行為還真的如他所述,更加引起我的好奇,因此挖進去看一下。這篇是追查的結果,也順便把我追查的過程記錄下來。

注意到其輸出的 SQL 看起來像是在進行 Nullify

DEVELOPMENT [11] rails101(main)> @group.group_users.delete_all
  SQL (0.1ms)  UPDATE "group_users" SET "group_id" = NULL WHERE "group_users"."group_id" = ?  [["group_id", 1]]
nil

找了一下 文件 卻是說 “This is a single SQL DELETE statement that goes straight to the database”,其 source code 看起來也符合其宣稱。根據文件裡提供的寫法試打看看,發現行為符合預期

DEVELOPMENT [12] rails101(main)> GroupUser.delete_all(group: @group)
  SQL (0.4ms)  DELETE FROM "group_users" WHERE "group_users"."group_id" = 1
0

看一下他們的 source code 來自何處

DEVELOPMENT [13] rails101(main)> GroupUser.method(:delete_all).source_location
[
    [0] "/usr/local/opt/rbenv/versions/2.2.2/lib/ruby/gems/2.2.0/gems/activerecord-4.2.5/lib/active_record/querying.rb",
    [1] 8
]
DEVELOPMENT [14] rails101(main)> @group.group_users.method(:delete_all).source_location
[
    [0] "/usr/local/opt/rbenv/versions/2.2.2/lib/ruby/gems/2.2.0/gems/activerecord-4.2.5/lib/active_record/associations/collection_proxy.rb",
    [1] 442
]

發現他們是來自不同檔案不同 methods:

追進 rails/activerecord/lib/active_record/associations/collection_proxy.rb 的 source code,在 method 前面的註解裡發現以下說明:

 # Deletes all the records from the collection according to the strategy
 # specified by the +:dependent+ option. If no +:dependent+ option is given,
 # then it will follow the default strategy.
 #
 # For +has_many :through+ associations, the default deletion strategy is
 # +:delete_all+.
 #
 # For +has_many+ associations, the default deletion strategy is +:nullify+.
 # This sets the foreign keys to +NULL+.

重點:

  • 如果沒給 dependent option 的話,會使用 default strategy
  • has_many 的 default strategy 是 nullify

也就是這個案例的狀況,因為原文提到 “要達成這個bug首先要把 has_many 的 dependent 參數拿掉”,且其 source code 的該 association 並未指定為 has_many :through (雖然照這幾個 tables 的開法應該是想要做 has_many_and_belongs_tohas_many :through?)

點進去 Blame 介面,找到以下幾個相關 commits:

  1. add CollectionProxy#delete_all documentation
  2. Do not invoke callbacks when delete_all is called
  3. Clarify deletion strategies for collection proxies

其中第 2 個 PR 裡提到這樣的用法:

DEVELOPMENT [15] rails101(main)> @group.group_users.delete_all(:delete_all)
  SQL (0.1ms)  DELETE FROM "group_users" WHERE "group_users"."group_id" = ?  [["group_id", 1]]
nil

結論

這是個 feature…

產生短但保證不重複的亂數 token in Rails x ActiveRecord x PostgreSQL

2015-09-07 09:00

Pic CC-BY-SA by Christiaan Colen https://flic.kr/p/x9G5bQ

為每個 Records 產生不重複亂數 token 是很常見的需求,一個常見的解法是使用 UUID,原則上就可確保不重複。但有時候會有「既要短、又要保證不重複」的需求,例如訂單編號(不希望外人可以透過自動遞增的 id 得知訂單量、但又要能透過電話唸給客服)。

有寫過一點 Rails 的人可能馬上就會想到在 Model 加個 validates :short_token, uniqueness: true 並在碰撞的時候用迴圈嘗試重新幾次,但這其實是不保險的:兩個 requests 同時發生的話會 race condition

這不保險的程度依照你的流量而定,網站流量很小的話可能前幾個月都不會遇到這種問題,但最好一開始就考慮進來,畢竟這種 token 欄位通常都很重要,一旦發生重複會有很麻煩的問題(業務會出包之類的)。

解決辦法

  • 利用 DBMS 的 unique constraint
  • 嘗試儲存被 DBMS 拒絕的話,產生新的亂數 token 重新嘗試
  • 因為要利用 DBMS 的 unique constraint,放在 after_create hook 比較合理

實做

實做的部分基本上是參考 Handling Token Generation Collisions In ActiveRecord 再加上 race condition 的考量。

幫 token 欄位加上 DBMS level 的 unique constraint:

class AddIndexToShortToken < ActiveRecord::Migration
  def change
    add_index :users, :short_token, unique: true
  end
end

after_create hook:

class User < ActiveRecord::Base
  validates :short_token, uniqueness: { allow_nil: true }

  after_create :set_short_token

  private

  def set_short_token
    return if short_token

    ActiveRecord::Base.transaction(requires_new: true) do
      update_column :short_token, SecureRandom.hex(3)
    end
  rescue ActiveRecord::RecordNotUnique => e
    @token_attempts = @token_attempts.to_i + 1
    retry if @token_attempts < 3
    raise e, "Retries exhausted"
  end
end

為什麼要 requires_new: true

如果你使用 PostgreSQL,這個就是必要的。如果你拆掉 ActiveRecord::Base.transaction(requires_new: true) do 這一層 transaction,你會看到 rails 噴出 ActiveRecord::StatementInvalid 之類的 exceptions。其原因是:

On some database systems, such as PostgreSQL, database errors inside a transaction cause the entire transaction to become unusable until it’s restarted from the beginning.

解決辦法:

In order to get a ROLLBACK for the nested transaction you may ask for a real sub-transaction by passing requires_new: true. If anything goes wrong, the database rolls back to the beginning of the sub-transaction without rolling back the parent transaction.

兩者都在 ActiveRecord::Transactions::ClassMethods 文件裡。

用了 ActiveRecord::Base.transaction(requires_new: true) do 以後,Rails log 會變成類似這樣(請注意 SAVEPOINT 指令):

-- Create transaction 的開始 (由 Rails 自動)
(0.6ms)  BEGIN

-- User.create(...)
SQL (0.7ms)  INSERT INTO "users" ("created_at", "updated_at") VALUES ($1, $2) RETURNING "id"  [["created_at", "2015-09-05 12:42:10.606633"], ["updated_at", "2015-09-05 12:42:10.606633"]]

-- ActiveRecord::Base.transaction(requires_new: true)
(0.1ms)  SAVEPOINT active_record_1

-- update_column :short_token, 'OOOXXX'
SQL (0.7ms)  UPDATE "users" SET "short_token" = 'OOOXXX' WHERE "users"."id" = $1  [["id", 57]]
PG::UniqueViolation: ERROR:  duplicate key value violates unique constraint "index_users_on_short_token"
DETAIL:  Key (short_token)=(SecureRandom.hex(3)) already exists.
: UPDATE "users" SET "short_token" = 'SecureRandom.hex(3)' WHERE "users"."id" = $1

-- end (ActiveRecord::Base.transaction(requires_new: true) 的)
(0.1ms)  ROLLBACK TO SAVEPOINT active_record_1

...

-- create 順利完成(如果失敗,這邊就會是 ROLLBACK)
(5.9ms)  COMMIT

為什麼要 uniqueness: { allow_nil: true }

如果你的 model 上線第一天就會有這個亂數 token 機制的話,你可能不需要這個,改成 uniqueness: true 應該就可以了。

但如果你有現有 records,那 uniqueness validator 加下去以後,short_token == nil 的 records 都會違反此檢查,意味著及使你沒有弄壞其他欄位,嘗試 .save 時仍然會炸在這裡。我為了 deploy 與跑 data migrate 的空窗期的考量,一開始先在這邊加上 allow_nil,之後其實可以拿掉。

之前還在想是否會因為 after_create 發生兩個 records 同時是 nil 的狀況?但思考 & 嘗試過後結論應該是沒有問題,因為整個 create 是自動被 rails 包在 transaction 裡的,commit 的時候也意味著已經拿到 token 了。如果有錯請糾正我。

附帶一提,關於如何 migrate data (而非 db 欄位) 之後也會發佈一篇文章說明我目前採用的方法。

為什麼要 return if short_token

這個是防止有人手賤對已經有 short_token 的 record 再次執行 set_short_token 導致被覆寫。

Bonus 1: 短 token 的設計

如果這個短 token 需要被人讀或透過電話告知,你可能會想考慮以下幾點:

  • 避免摻雜太多符號(因為符號可能有蠻多別名)
  • 避免同時使用 O, 0, I, l, 1, 8, B,因為他們長得很接近,容易搞混。
  • 如果要用口頭唸出來,則不能分大小寫
  • 綜合以上幾點,其實 Base32 很符合需求,主流的定義 (RFC 4648) 基本上就是 [a-z][2-7],有 gem 可以直接用。成果類似 Base32.encode(SecureRandom.random_bytes).first(7)
  • 7±2 原則應該大家都聽過,這邊不講解心理學上的細節,總之長度在 5 ~ 9 個字之間會比較方便好記,而且也容易一口氣唸完。
  • 決定長度時要考慮可容納的 token 數。我的想法是 (32^(字串長度))/2 = 可安全容納的 token 數(每次 generate token 的碰撞機率 < 1/2)。

Bonus 2: 如何比較其效果

  1. 開一個 rails 專案,在 localhost 準備好 produciton 環境
  2. 寫一個簡單的 API,且總是嘗試塞同樣的 token
  3. RACK_ENV=production puma (當然要先裝好)
  4. 用以下 script 生 multi threads 去打那個 API
require 'net/http'

uri = URI('http://localhost:9292/users/generate')

5.times.map {
  Thread.new { Net::HTTP.get_response(uri) }
}.each(&:join)

其他解法

其實這不是唯一的解法,例如 JokerCatz 就用 Integer Obfuscator 來解這樣的問題(簡單來說就是 hash function),在一定範圍內不會碰撞,因此有寫入速度快的優點(不用依靠 DBMS 的 unique constraint)。但小弟服務的公司有一些考量,最後還是採用不可逆 (隨機亂數) 的解法,這篇是採用此解法的筆記。

Project Management for Freelance Developers 課程筆記

2015-05-25 10:30

最近在 Learnable 網站修了一門課 Project Management for Freelance Developers,雖然是很基礎的 Project Management,但做為入門基礎還算不錯,以下是我做的筆記重點。

Lesson 1: Course Introduction

  • N/A

Lesson 2: Standardized Methods of Project Management

  • 介紹 Agile, Lean, Scrum, Waterfall 的基本精神
  • 但身為 Freelancer 的話,以上的方法論都不可能整套採用,畢竟你要同時對多個客戶(如果你只有一個客戶,那有點危險,雞蛋不要放在同一個籃子)

Lesson 3: Managing Projects

Step 1: Creating Your Own Project Management System

  • 身為 Freelancer,除了做客戶的需求外,拓展自己的 businesses 也很重要
  • 訂下 Email 處理原則,收到普通重要度的信也立刻回覆,會很浪費時間(尤其打斷 flow 的話),一個禮拜可能清空一兩次信箱就 ok 了
  • 批次作業:安排時間回覆所有 email,一大段時間拿來寫 code,一大段時間拿來寫文章等。而不是寫一下 code 回一下信再看個影片這樣快速切換
  • 每週一次 review 你的 Project Management System 哪部分 work 哪部分不 work,寫下來,寫下改善方案,並在下一週實行
  • 尋找到 work for you 的選項後,開始長期實行,但每個月跟每季仍要 review 一次

Step 2: Managing Your Own Projects

  • 避免 scope creep (客戶把追加功能當加水免錢似的)
    • 要制訂許多防衛原則,例如若因為客戶方太晚回應導致時程拖延,延長的時數將會變成更高的鐘點費等,這些事情值得你花錢請律師
    • 討論後要寄 email 把你理解的內容跟客戶確認,確認後才實作
    • 減少 stakeholders,不然你一言我一句,email 一個一個提供建議就沒完沒了了
    • 不過當 project 所有問題都要經過同一個人的時候,那個人可能成為瓶頸(老是很慢回信),也是要避免的問題
  • 讀這篇文章: http://www.ndoherty.com/effective-email-communication/
  • 記得要留處理雜事的時間,例如接洽客戶、提案、處理發票等,因為在公司工作的時候通常這些事不需要你做

Step 3: Managing Other People

  • 溝通:assign 工作的時候,用清楚簡單的語言,而不是假設對方有相關專業知識用語。該解釋的要解釋之外,也預測對方會有什麼疑問,不能期待別人有通靈能力
  • 文件:用文件、截圖、螢幕錄影等方式,把需要重複解釋的東西變成 HOW TO 文件
  • 把任務拆成小 task,押明確的日期,不要把一個 mini project 等級的東西 assign 給不適合的人

Step 4: How to Plan Your Own Projects

  • 依照 kickoff 會議的 outline 決定 milestone
  • 但 milestone 應該是一些 deliverable(例如 wireframe、prototype、第一次 revised、成品等),而不是「25% Done」這種東西,因為 25% 的內容是什麼每個人的解讀不同
  • 要有計畫,但計畫太詳細可能會浪費時間,因為 project 有可能會轉向。所以先 plan 大致、與 milestone,接近的時候才 plan 詳細的 tasks

Lesson 4: Project Management Tools

  • 介紹 Basecamp, Asana, Wunderlist 的特色
  • 提出一些尋找適合自己的工具時的考量點

Lesson 5: Course Conclusion

  • N/A

部落格搬家筆記: 改用 Jekyll 並透過 wercker 自動發佈到 S3 上

2015-02-22 10:00

過年在老家無聊的時候開始處理一些堆積已久的數位雜事,部落格搬家是其中一個特別大條的,從著手進行到可公開程度就花了 3 天多,搬家後第一篇當然就是搬家記錄囉!

技術規格跟用到的服務

  • Jekyll 2.5.3
  • Amazon S3 (作為 Static Website Hosting)
  • Amazon CloudFront (加速)
  • BitBucket (放 Jekyll project source code)
  • Wercker (BitBucket 有更新就會自動 deploy 到 S3)
  • 其他用到的基本服務
    • Disqus
    • Amazon Route53
    • Google FeedBurner
    • Google Analytics
    • Google Custom Search
    • Google Webmaster

特殊功能

其實用別人的部落格平台還是比較方便的,不用自己管網站、機器、特殊字元、發佈機制等,但為什麼我還是換成 Jekyll 呢?其實這提供了幾個特殊功能,是我一直想要、但透過別人平台很難達成的:

  • 有預設的全站 og:image,但單篇文章可以單獨指定 og:image (範例: 使用預設自訂)
  • 首頁與 RSS feed 分中英兩組、Layout 可雙語切換、部分文章可雙語切換 (範例)
  • 作品集專頁,可以高度客製化 (畢竟 HTML 控制權在我手上,下次有空就會料理它)

接下來就是一些搬家過程筆記了

從 Logdown 搬來的注意事項

首先要注意 Logdown 匯出檔案有一些潛在問題:

  1. 文章 title 內有單引號會變成不合法 YAML,遇到時改成用 double quoutes 包字串即可解決
  2. Front matter 裡雖然有 published: key,但就算是草稿還是會給 true
  3. 匯出檔會包含不該存在的 posts,猜測是因為用 soft-delete 機制沒注意到 scope

因此下載匯出檔後,務必檢查上述幾個問題,可以跟 Logdown 後台文章列表對照,針對第 3 點可以在 inspector console 下列出所有文章,整理格式後,跟 ls _posts/ 的結果 diff,可較快找出不該存在的文章。

$("li.post.group.published a.btn-link").each(function() { console.log($(this).attr('href')) })

接下來利用鴨七的 hikkoshi Blog Migration Tool 把 Logdown 匯出檔轉成 Jekyll 格式。

但這樣還沒完,還有一些問題要處理

  • 如果你是要連 domain name 一起轉到新家,要確保舊有文章不會壞掉,可在各文章的 front matter 段落裡指定 permalink
  • Logdown 會自動把 # 翻成 h2,但 Jekyll 不會,一個頁面有多個 h1 的話對 SEO 有負面影響,所以要人工把 headers 退位
  • 原本站內互連文章的超連結要改掉,這部分可利用 post_url 會比較方便 (文件)
  • Jekyll 提供的 markdown renderer 都不支援 code block 的 file title,鴨七因此寫了 給 HtmlPipeline 的專用 filter,但我因為一些考量只想用 Redcarpet,卻又沒有理想的解法,所以我直接捨棄 file title,把它移到 block 內變成註解,需手動處理。

Jekyll

基本設定

依照 Quick-start guide 把 Jekyll 在 local 架起來。

此時你可能有想改的 Jekyll default 行為,幸好 Jekyll 文件寫得還蠻清楚的,這幾篇有一些基礎功能可以參考:

其中 markdown renderer 因故我選擇了 Redcarpet (目前預設是 kramdown),在 Configuration 文件裡有設定教學。

另外樣版裡 {% if ... %}, {{ var }} 等是 Liquid 語法,套版會用到的基本語法可參考 Liquid for Designers

另外還有一件小事,生成靜態檔時,除了程式跟一些預設的東西外,一般檔案會被 copy 到 _site 底下,因此要確認 _site 下有無不該被生出來的檔案,例如我為了要用 wercker 就增加了 Gemfilewercker.yml,但這些檔案不是對外網站的一部份。

開啟常用 Redcarpet extensions

我開啟了一些外掛方便 markdown 撰寫,功效可參考 Redcarpet Readme

# in _config.yml

markdown: redcarpet
highlighter: rouge

redcarpet:
  extensions:
    - strikethrough
    - autolink
    - no_intra_emphasis
    - tables
    - with_toc_data
    - safe_links_only

如何在單篇文章內指定 og_image

前面提到的三個我一直想要的特殊功能,雙語切換可能大部分人都不需要,page 基本上也是 Jekyll 內建功能改改樣版而已,og_image 是我覺得比較值得分享一下的,對照 Jekyll 跟 Liquid 文件應該就能看懂,所以就不詳細解釋了:

# in _config.yml

title: Site title
description: Site description
# ...
og_image: "/images/default_og_image.jpg"
# front matter of a post
---
title: ...
date: ...
published: true
# ...
og_image: /images/xxx-post-excerpt.png
#           or
# og_image: http://example.com/images/xxx-post-excerpt.png
---
<!-- in _includes/head.html -->
{% if page.og_image %}
  {% if page.og_image contains 'https://' or page.og_image contains 'http://' %}
    {% capture og_image %}{{ page.og_image }}{% endcapture %}
  {% else %}
    {% capture og_image %}{{ page.og_image | prepend: site.baseurl | prepend: site.url }}{% endcapture %}
  {% endif %}
{% else %}
  {% capture og_image %}{{ site.og_image | prepend: site.baseurl | prepend: site.url }}{% endcapture %}
{% endif %}

<!-- ... -->

<meta property="og:image" content="{{ og_image }}">

這部分有一些參考自 Getting social with Jekyll 這篇文章,但他使用到的技巧更多更複雜。

用 Wercker deploy 到 Amazon S3 上

設定 S3 Static Website Hosting

參考這兩篇文件用 S3 架設起一個測試網頁(架設過程先隨便寫一個 index.html 透過 Amazon 後台上傳即可),並把 custom domain 設定好:

設定 Wercker

Wercker 是個幫你依照設定檔 deploy 的服務,當初注意到這個服務是因為 Hugo.io (Go 語言寫的 static site generator) 官方推薦用 Wercker deploy,覺得似乎不錯,因此想拿來跟 Jekyll 搭配使用,實際上也早有人做過這種組合。

參考 Simplify your Jekyll publishing process with wercker and Amazon S3 設定。

完成的 wercker.yml 應該會類似這樣:

box: wercker/rvm
build:
    steps:
        - rvm-use:
            version: 2.0.0

        - bundle-install

        - script:
            name: generate site
            code: bundle exec jekyll build --trace

deploy:
    steps:
        - s3sync:
            key_id: $KEY
            key_secret: $SECRET
            bucket_url: $URL
            source_dir: _site/

其中 bucket_urls3cmd 在用的格式,在 S3 bucket 的 unique name 前面加上 s3://,以我的例子而言,就是 s3://toyroom.bruceli.net。不過這些設定不建議直接 commit 進 git,因此官方教學也是教使用 environment variables。

效果

設定 404 page

如果 deploy 順利的話,你現在應該可以看到 S3 上的網站了,不過此時你隨便打個不存在的網址的話,會看到很醜的錯誤畫面。

製作一個 404.html 頁面,並在 S3 後台指定 Error Document 為 404.html

設定 CloudFront

由於是作為公開網站,為了加速一般都會建議過 CDN。

基本設定

參考 Using CloudFront with Amazon S3 文件設定好並等待生效。

觀察下列指令結果是否有出現 CloudFront 的 headers:

curl -I http://your.domain.name/index.html

除了 default root object 以外的 index file 都讀不到了

參考 Why won’t index files deployed on Cloudfront work like it does in S3? 一文把預設的 Origin 改掉即可。

但接下來會遇到 Cache Invalidation 的問題,因此還是建議避免依靠 default index file (root 除外)。 下一節會補充解決方法

Cache Invalidations 問題

過 CDN 後就出現一個比較麻煩的問題:CDN 會 cache 住網站,browser 再怎麼 refresh 都沒有用,所以動到網站後就要通知 CloudFront 清 cache。

架站過程我暫時都去後台手動 invalidate,有幾個注意事項:

  • 似乎不支援 wildcard 字元 (***)
  • 所以我用 find _site -type f 列出所有靜態檔案再整理格式 (用空白或換行分隔)
  • //index.html 各自算一個 object,因此要分開列出

2015-05-21 起支援 wildcard 了,但有一些限制,請參考 gslin 大大的重點整理官方公告

但長期來說這樣是很麻煩的,我之所以想用 Wercker 就是希望必要時可以 直接在 BitBucket 線上編輯器改好後,剩下的 build, release, invalidate cache 就自動完成

研究一下後發現官方的 s3sync step 有個相關 issue: Use newer s3cmd version,大意是說 Wercker 使用的 s3cmd 版本太舊不支援 --cf-invalidate option,因此有人寫了自己的 step,把底層換成 s3_website gem。

補言:官方 s3sync 已經大更新,現在可以支援 --cf-invalidate option,我轉回去的過程很順利,只是 invalidate default index or path 的行為跟 s3_website 不一樣,需要自己加上參數,底下會介紹到。

確認了一下他的成果,發現似乎不適用於我採用的 Wercker box,所以我 又 fork 了自己的版本 ascendbruce/step-s3sync ,使用上有一些前提:

  • 需使用 rvm box
  • Gemfile 內要包含 gem "s3_website"
  • wercker.yml 需修改內容如下(Wercker 後台變數也要追加)

    diff deploy: steps: + - rvm-use: + version: ruby-x.x.x + - bundle-install - - s3sync: + - ascendbruce/s3sync: key_id: $KEY key_secret: $SECRET source_dir: _site/ - bucket_url: $URL + bucket_name: $BUCKET + cf_distribution_id: $CF_DISTRIBUTION_ID + region: ap-northeast-1 # 這個是 S3 的 region

官方文件有教怎麼寫自己的 step: Creating your own steps,開發過程我有遇到一些小細節問題補了好幾次,如果想寫自己的版本的話,可以加減參考我的 commit log。

換成改過的 step 後,每次 sync 到 S3 後就會自動打 CloudFront API,就觀察似乎不是無差別全打,但不清楚是怎麼判斷的。

補言:應該是依照跟 S3 sync 時有更動的檔案。

subdirectory 的 index.html 預設並不會自動幫你送成 /subdir/index.html/subdir/ 兩個,參考 s3cmd Usage 文件 加上你需要的 option:

  • --cf-invalidate-default-index: invalidate /subdir/index.html
  • --cf-no-invalidate-default-index-root 不 invalidate /subdir/ (因為預設會)

另外免費 invalidation 是有額度限制的,目前 收費標準 如下

No additional charge for the first 1,000 files that you request for invalidation each month. $0.005 per file listed in your invalidation requests thereafter.

最後,展示一下自動發佈的總成果,這篇文章是這樣發佈的:

  1. 在 BitBucket 上 Merge PR (到 master branch)
  2. 自動 jekyll build > sync to S3 > invalidate CloudFront cache > 完成

以上就是這次搬家的筆記,喜歡的話請按個讚唄~

補言

  • 有朋友問為何不用 GitHub page,我的主要原因是不希望 drafts 跟修改記錄被大家看到,而 GitHub 的 private 方案還蠻貴的,所以不太考慮。這點也是 Jekyll blog Automatically generate and publish on AWS S3 and CloudFront 這篇文章的理由之一。另外,這篇文章沒有主動打 CloudFront invalidation,而是在官方的 s3sync step 加上 Cache-Control header,效果跟省錢效果不知如何,過幾個月有空的話也許可以試試看。
  • 之前一直覺得上 CDN 可以節省 S3 費用,但看了一下目前的帳單,發現這個不知道從哪聽來的經驗似乎不適用於貴森森的 Amazon CloudFront,所以只好當「享受加速的好處」了。

《超效率!生活習慣》電子書開賣囉

2015-01-06 14:24

我大約從去年 10 月開始寫的電子書 《超效率!生活習慣》 開始販售囉!

更新:這本書完成囉!

經過五個月的補充,目前已經推出完成版了,之後將會掛著完成度 99% 持續做小幅度補充。原本擔心本書連載會腰斬的朋友,現在歡迎購買~ 《超效率!生活習慣》

本書的主要內容是各種效率技巧,包括生活撇步、推薦購買的生產力工具、懶人理財、Mac 設定、行事曆使用、個人 Issue tracking、工作 Issue tracking、檔案目錄配置原則、備份原則、程式設計師效率等;總之就是包山包海的效率建議的精華集。點進去的產品頁就有更詳細的介紹了,所以這篇我想講一些背後的故事。

為什麼會想寫這本書

主要是我偶爾會聽到別人在抱怨生活上有個問題很繁瑣,或者聊天過程中聽到對方的生活習慣很沒效率,情況允許的話我會提出建議,但有時候不是那麼適合,而且也會搞得好像我意見很多似的,於是乾脆來個意見大爆發。對!我意見就是很多。

另一方面則是我想嘗試看看 Lean 的精神 —— 快速出貨,快速改進商品,所以如果您有想知道的類別、覺得應該補充的項目、覺得我解法不夠好的地方都歡迎跟我說。在本篇文章底下留言或寄 Email 都可以:

書名是怎麼來的

這也是我想實驗的事情之一:投放 Google 廣告,依照點擊率決定產品名稱

最後是由 19% 的 “超效率 生活習慣” 勝出(Google 廣告標題禁止包含驚嘆號),其他的比率分別是 17%, 15%, 11% (中途有刪除掉兩個太冷門的候選標題)。

附帶一提,原本我意屬的是 15% 的那個,還好我有投廣告決定。

定價是怎麼決定的

10 月左右的時候,我問過幾個技術圈的朋友願意用多少錢買這樣的書,大部分回答都出乎我意料的高,也許是習慣技術書的價錢吧    

不過我最後並沒有直接採用問到的平均價錢,而是再低一點點的價格,大致上的考量有:

  • 台灣實體書通常會打 66折 ~ 79折
  • 台灣實體書標價大概 250 ~ 360
  • Leanpub 上某本停滯很久的未完成中文書的售價還掛在 6.5USD

希望實際售價能跟上 Leanpub 裡面的售價(一個字:貴),但又不要偏離台灣實體書的價錢太遠,於是:

N * 5折 = 6.5USD => N = 13USD => 標價 = 12.99USD,打 5折 ~ 79 折

嗯,真是隨性啊。

最後呢,由於這本書很多方面都是我第一次嘗試,寫這篇文章的時候都還沒有發生任何一筆賣書收入,其實有點擔心能不能回本(封面的圖片版權、投放 Google 廣告、時間成本等),因此,方便的話請幫我一個忙:任選做一件以下的事情(複選也歡迎)

  • 按本篇文章 FB 讚
  • 轉貼本篇文章或 Leanpub 上的介紹頁 到您的 Facebook timeline / Twitter / Plurk / Google+
  • 私訊您所知、對這種書會有興趣的朋友
  • 購買本書(趁現在還是 5 折的時候)

謝謝您的支持,也希望這本書有幫助到您。

Sublime Text ERB end[tab] 自動完成的關鍵字衝突問題

2014-12-10 02:44

本來想寫一篇部落格文講這個問題的,但寫到一半跑去開 PR 給 ERB-Sublime-Snippets 增加說明,就這樣被接受了。

因此請看 ERB-Sublime-Snippets > README > Resolve conflicting tab trigger 小節。

主要的問題點是,我同時裝了 ERB-Sublime-SnippetsRails Developer Snippets 這兩個 packages,再加上內建的 Rails package,他們有幾個 tab 自動完成關鍵字是相同的,其中最常遇到的就是 <% end %> 了。

解決方法就如連結裡所寫。

另外,我仍然 fork 了自己的 ERB-Sublime-Snippets 版本 (https://github.com/ascendbruce/ERB-Sublime-Snippets),原因是作者不接受 erperc 這兩個關鍵字 (預設的是 er, pepc)。我個人是覺得 erppe 較直覺啦,所以… 歡迎使用,不過我還沒更新 README 的安裝教學,可以先參考 這個

T客邦目前使用的 code 品質輔助工具

2014-12-08 02:42

這篇介紹一下T客邦跟我個人有在使用的品質(尤其是 coding style)輔助工具,有些是我幫T客邦引入的,有些是我從T客邦現有的制度學來的。

RSpec

比起這篇要介紹的其他項目,測試算是不太一樣的類別,但它很重要。例如改 Apple modle 意外的搞爆 Banana controller 而不知,如果你沒寫測試,可能就會到上線後才知道。

小作業:去查 regression testing 跟 non-regression testing 是什麼

寫這篇的時候,T客邦的測試大部分還是很陽春的狀態,覆蓋率不高、寫法也沒有特別遵循一些已知的 guidelines,但即使是最基本的測試也可以享受到一些好處。T客邦的測試寫法還有很大個改善空間,就等優秀的你來應徵。(☉▽☉)

加入 Gemfile

# Gemfile
group :development, :test do
  gem 'rspec-rails', '~> 3.0'
end

bundle install

跑 generator

rails generate rspec:install

先寫一個很陽春的 spec,改到 rspec 可以順利跑完為止

# spec/controllers/posts_controller_spec.rb
require "rails_helper"

describe PostsController, type: :controller do
  describe "GET 'index'" do
    it "renders index template" do
      get :index
      expect(response).to be_success
    end
  end
end

之後再視需求追加測試、追加 gems (例如 capybara, faker, fabricator 等)。如果你的目標是確保執行路徑上不要有嚴重錯誤就好,可考慮寫 feature tests 為主。

如果你使用 capistrano 做 deploy,但沒有架 CI server 的話,可以參考 Use codeclimate-test-reporter without a CI server 把 run_tests 的 capistrano task 設定好,如此一來若有忘記跟著功能程式碼一起修改的 test 敗壞的話,就會阻止你 deploy。(這部分是學自 Reliably Deploying Rails Applications 一書)

Coding style, code smell 與 best pratices 工具

這幾個項目其實都不用寫在 Gemfile 內,但我習慣直接加進去,因為只要 bundle install 就都裝好了,且以後新人即使一開始不知道有這些 gems 可用、只要肯搜尋各個 gems 的用法就也會發現。

修改這些警告項目時,如果有不錯的測試覆蓋率將會增加修改的信心,反之就會很擔心會不會搞爆功能了、最後還是不敢動它。如果你維護的專案既沒有測試也沒有遵循 coding style,我會建議:

  1. 先修嚴重的問題,例如 brakeman 警告中,特別嚴重的安全問題
  2. 補一些陽春測試,確保執行路徑上不要有嚴重錯誤,並且跑不過就不准 deploy
  3. 之後再開始修次要 coding style

Brakeman

是用來偵測 Rails 寫法造成的安全性問題的工具。

# Gemfile
group :development do
  gem 'brakeman', :require => false
end

bundle install

在 project 根目錄執行

brakeman

就會看到它提出的建議,似乎大部分建議也都可以在官方的 Ruby on Rails Security Guide 看到。

rails best pratices

是依照社群維護的 Rails Best Practices 提供寫法建議,常見的建議有:在 partial 內不要用 @instance_variable使用 render 的簡化寫法應該從 view 移到 model / controller / helper 的 code移除行尾空白 等。

# Gemfile
group :development do
  gem 'rails_best_practices', :require => false
end

bundle install

在 project 根目錄執行

rails_best_practices

參考官方 README > Customize Configuration 把你確定不處理的警告關掉。

EditorConfig

是指定空行、換行 style 的編輯器外掛+設定檔。雖然各編輯器通常可以自己設定,但使用 EditorConfig 的好處是只要要求新人裝對應的外掛即可,不怕新人設定錯,而且設定可以跟著 project 走。

在 project 根目錄加設定檔

# .editorconfig
# Please install EditorConfig plugin for your editor or IDE
# Usage and plugin list can be found here: http://EditorConfig.org

# top-most EditorConfig file
root = true

[*]
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
indent_size = 4

並到 EditorConfig 官方網站尋找你使用的編輯器,安裝相應的編輯器外掛。

幾個月前我第一次嘗試使用 EditorConfig 時發現 Sublime 的 global settings 會覆蓋掉 EditorConfig 的設定,但第二次嘗試時就正常了,不確定是誰 update 過。

rubocop

依照社群維護的 bbatsov/rails-style-guidebbatsov/ruby-style-guide 提供建議。常見的建議有各種縮排層級、空白、空行的位置/數量、統一使用單引號或雙引號(依照你的設定) 等非常詳細的 coding style 問題。

# Gemfile
group :development do
  gem 'rubocop', :require => false
end

bundle install

在 project 根目錄執行

rubocop

就會看到修改的建議

參考 官方 README > Configuration 把不修正的警告關閉、與團隊現有 coding style 不符的地方改參數。另外也可以在誤判的程式碼前後加上特殊的註解即可小範圍排除特定的警告。

另外,Rubocop 具有自動修正的功能,我使用以下的流程(實際指令要去翻 README):

  1. 寫一個把所有 cop 都關閉的設定檔
  2. 寫一個繼承上述設定檔,並且 override 掉想修正的項目(但不是所有 Cop 都支援自動修正)
  3. 執行 Rubocop 的自動修正指令
  4. 回到第二步,改成下一種要修正的項目

可用的外部服務

這部分是一些可以幫你跑檢測工具,並透過較友善的介面回報給你的服務。

Hound

是 Rubocup + CoffeeLint + JSHint 的服務 且偵測到問題會自動在 Pull Request 上直接留言

計費方式

  • 12USD per project per month
  • 有 Open Source 可以自己架

Code Climate

是 Brakeman + 複雜度與重複性 的網路服務 其介面設計的很漂亮,且若有誤判可以從網頁介面隱藏掉(如果你自己跑 brakeman 的話,可能就要自己記住那個其實是誤判等等)

計費方式

  • 5 users x 5 private repos = 99USD per month
  • 16 users x 10 private repos = 199USD per month
  • 32 users x 20 private repos = 399USD per month

PullReview

還沒有認真的用,但初步看起來,使用方式跟 Code Climate 類似、功能較多,但計費方式不同。 功能似乎相當於有 Rubocop + Brakeman + 複雜度與重複性檢測,但底層是用誰目前不確定,支援 GitHub 跟 BitBucket。

  • 1 user x unlimited private repos = 20EUR per month

其實還有一些其他的服務,但因為我自己沒用過所以就不特別介紹了。

Site Search
Blog Archives