What have you found for these years?

2009-01-01

cache (4)

人人要跨年我卻從早上寫程式到半夜不止歇。這晚點再講。

為了做朋友的相片 cache, 連朋友 relation 都要做。
所以做了 self-referential, 然後發現這樣做好像只是單向,
而不是雙向的。平時 A => B 或 B => A 的關係是固定的,
然而 self-referential 卻會變成 A 是 B, B 也是 A,
因此似乎變成需要兩筆相反的 edge?

但我覺得這樣做很蠢,所以改成用兩筆 query 來組成結果。
也就是 A => B, B => A 各做一次,然後合併起來。

  def friends
friends_true + friends_false
end

不用介意什麼是真假朋友,只是為了讓 !A 為 B 而這樣做而已。
畢竟如果用 n * -1 的話,名字裡可不能用 '-' 哩。

於是真假朋友這樣做:
[true, false].each{ |b|
friendships = "friendships_#{b}".to_sym
has n, friendships,
:class_name => 'Cache::Friendship',
:child_key => ["user_#{b}_id".to_sym]

has n, "friends_#{b}".to_sym, :through => friendships, :class_name => 'User',
:child_key => ["user_#{b}_id".to_sym],
:remote_name => "user_#{!b}"
}

DM 有個地方比 AR 煩,就是改寫一個名字,大概就要全部改寫,
因為預設的值都怪怪的... 於是會顯得稍微 verbose 一些。
這個缺點希望在日後的版本中可以改進。這點 AR 是做得比較好。

friendship 就很簡單只是:
belongs_to :user_true,  :class_name => 'User', :child_key => [:user_true_id]
belongs_to :user_false, :class_name => 'User', :child_key => [:user_false_id]

有了這個後,就可以開始 cache friendship 了。
require 'cron/ffbapi'

module Sync
module User
module_function
def friendship user
Ffbapi.use user.id
Facebooker::User.new(user.id).friends.each{ |friend|
# left?
Cache::Friendship.first( :user_true_id => user.id,
:user_false_id => friend.id ) ||
# right?
Cache::Friendship.first_or_create(
:user_true_id => friend.id,
:user_false_id => user.id )
}
end
end
end

增加的部份是多個 User/Photo 的 module, 因為這次是要以 user 當主角。
Ffbapi 是 facebooker 的 adapter, 拿來銜接樂多朋友的 api.
這邊我幹了不少蠢事:

1. 灌了新版 facebooker:

gem install mmangino-facebooker --source http://gems.github.com

因為直接用 plugin 上的會需要 rails 環境,亂改一氣是可以弄起來,
拿掉(或說補上)RAILS_ROOT 和 RAILS_ENV 就可以了。不過實在很討厭這樣做,
所以還是灌了 gem, 而 rubyforge 上的 0.9.5 好久了,想用新版...

結果灌了之後怎麼試都回答我 api key 錯誤。而 plugin 版卻可以!?
而且跑的速度變好慢!試半天,就在快要放棄時才忽然想到,啊咧,plugin
我改過啦!api url 從 facebook 改成樂多朋友囉。所以才說不想改 plugin 嘛,
這樣要換新版或是移植什麼的都變得好麻煩。

不過會這樣搞半天有一個很大的原因是 api 本來也是我寫的,害我一直翻程式碼出來,
看半天覺得沒有錯啊!結果造成這個大盲點。其實我覺得所謂 pair programming,
最好的地方就是可以避開這種極蠢的盲點。就算不大會寫,也應當記得要改 url.

於是照著 facebooker 的 adapter 做個 subclass, 只改兩個路徑即可。
又發現奇怪,為什麼朋友資料抓回來都怪怪的呢?這又是另一個盲點,
session_key 根本都還沒建立,而我 api 本來就會忽略這個錯誤,
所以這邊也搞半天才想起來 session_key 沒設。參考了 source code,
發現其實很簡單,去掉 rails plugin 的骯髒手法外,facebooker 似乎還算
可以修修改改:

s = Facebooker::Session.new api_key, secret

就可以產生一個全新的 session. 而:

Facebooker::User.new id

就有 user 可以用。第二個參數是 session, 所以:

Facebooker::User.new id, s

即可產生可以用 api 的 user. 不過也可以設在 current session 中:

Facebooker::Session.current = s

那麼 user 會自動取得:

Facebooker::User.new id

就有 session 可以使用。adapter 的套用:

Facebooker.current_adapter = Ffbapi.new({})

那個 hash 不要管,一些 config 而已,例如 api_key 之類的。
不能省略讓我好失望 XD

接著就可以這樣建立 session:
Facebooker::Session.current = begin
environment = YAML.load(File.read('config/thin.yml'))['environment']
config = YAML.load(File.read('config/facebooker.yml'))[environment]

Facebooker::Session.new(config['api_key'], config['secret_key'])
end

根據 thin 的 environment 來決定要用哪個 env, 我所有的都這樣寫。
再加上偽造的 auth_token/session_key:
  def Ffbapi.use id, s = Facebooker::Session.current
s.auth_token = id.to_s
s.secure!
s
end

這個就是小撇步了,原本的流程是:

1. auth.createToken
2. login
3. auth.getSession

不過在 ffbapi 裡,login 的部份是完全沒用,改以 cas 完成。
http://en.wikipedia.org/wiki/Central_Authentication_Service
於是這邊的流程會變得有些弔詭。暫時先不深究,目前重心放在 cache 上。
這邊要完整實作還有一段距離... 如果我沒記錯的話,呼叫 auth.createToken 後,
cas 應該要有辦法取得剛剛的 token, 然後在 login 後丟回給 client.
也就是說需要動到 cas server, 目前這部份不是我處理,那就先跳過...

觀察這邊的流程其實也費了我不少功夫。facebook 的認證算是滿完善的。
當然啦,我想大部份的認證應該都很完善,只是這方面我不熟,也是看很久才搞懂
為什麼要做得這麼複雜。每一個小細節都是防止一種攻擊法,全部加起來就算滿健全的。

所以這邊我暫時作弊!跳過 auth.createToken, 也跳過 login,
直接用 auth.getSession. 那這 session 怎麼產生呢?答案是根據 auth_token
建立。app 丟來什麼 auth_token, 就照樣做成一個假的 session_key.

也就是說,這使得 app 可以偽裝成任何人!這個呼叫應為危險的,
所以暫時只開放 private ip 連線... 其他 ip 一律丟 403 回去。
而上面提到的:

s.secure!

就是拿剛剛的 token 做成 session key. 成功偽裝...
這樣做的原因是朋友資料是看 session key, 而不是 pass uid.

回到 Cache::Friendship, 於是需要偵測真假(左右?前後?),
如果有缺的話就幫他補上,哪一個方向都無所謂。

這邊欠 remove 的動作,會使得絕交不會反應出來。還要待我思索怎麼做,
現在再寫下去就可以看日出了,所以先暫停。日後也希望把 facebooker plugin
拿掉,改成類似這邊 ffbapi adapter 的做法。

光以上,沒幾行程式,就費去我大半天的時間。我在想是我變笨了呢,
還是這本來就這麼複雜?早上寫到卡很久,只好再拿出紙筆出來鬼畫了一下。
重新調整後就變成這樣的結構了。

接下來就可以真的處理訂閱的部份,透過 photoship.
這邊我原本一直寫成用另外一個 repo, 所以才會有 cache (3) 那篇的東西。
結果用了一堆方法,怎麼做都不太理想。因為 DM 的 repo 似乎不能分 property.
也就是說,新出來的 friends_photos 需要多一個欄位,紀錄 watcher 是誰?
而這會使得 :default repo 也需要一個 watcher 欄位...

因為 DM 跨 repo 只能是 mapping 方式不同,而不能是 property 也有異。

repository(:friends_photos){ property :watcher, User }

這樣是不行的,會造成 :default 也有效果,我也不想多開新欄位,
patch DM 又太花時間了,只好先放棄這個。改成繼承,失敗得更慘,
因為連 auto_migrate! 都失效了,storage 只會出現一個。

module? 那又要處理 property holder, 有點煩。
想到 dm-is-remixable, 可以做些程式碼重複利用。
不過這個做法好像是拿來取代 rails 的 polymorphic association,
會產生新的 class 出來,看起來是對不太起來,要改也一樣是麻煩。

又想了很久很久,也試了很久很久,忽然間恍然。其實根本不用這麼麻煩,
所以資料可以都用同一份,這樣連 expire 的問題都不用考慮了。
cache 另一個大麻煩就是 expire 的機制啊!

而 comments_count 可不可以照用?說不定可以,把 counts 偷存在
relationship 中。搞不好就真的做得像 ada 說的一天存一筆 count,
有 post comment 時才更新資料,就可以抓出七天的資料再 sum 起來。
這樣甚至可以省去全資料掃描的動作,只是又有更多東西需要處理...
像是 compact 之類的動作。這,晚點再說吧... cache 做下去真的做不完。

不過說到這,忽然間就發覺 google analytics 超難做啊!
能在 google 裡寫這些東西的話應該會滿有趣的吧..?

總之,前面試了那麼多種方法,用 has n, :through 是會比較慢沒錯,
但省去很多複雜度,資料庫效能我不熟,但至少程式這樣寫是真的漂亮得多。
寫程式真的有很多技巧性的東西,看你有沒有想到而已。想到就能事半功倍,
否則就要用各種怪招,陷在泥沼、焦油坑裡面掙扎罷了...

然後 there's always a deadline, 也不能無止盡思索,
所以就變成邊寫邊想,design + programming + refactoring 同時進行。

簡稱:OO D P R XD

前面搞得很複雜,想到這招後就很簡單了:
module Cron
module Photo
module_function
def photoship photo
photo.user.friends.each{ |friend|
Cache::Photoship.create( :photo_id => photo.id,
:user_id => friend.id,
:created_at => photo.created_at ).compact!
}
end
end
end

多一個 created_at 紀錄 photo 的 created_at, 僅紀錄最新幾筆,
比較舊的資料就全部刪掉,所以最後接個 compact! 實作如下:
def compact! n = 3
user.photoships( :order => [:created_at],
:limit => 1000,
:offset => n ).destroy!
end

預設只留三張。那個 limit 好像不能省略,省略會噴 SQL error @@
那就隨便打個數字上去。想想也不敢打個 100000000000 上去,
要是真有那麼多那也太浪費時間了,一次砍 1000 很快也可以砍完。

接下來就把其中的一些東西移植到 AR 上就可以正式運作了。
不過剛才試跑了一下,發覺可能還需要調整,因為資料量超大! @@
現在才一點測試資料而已,cache_friendships 就衝到兩萬多筆 @@
稍微估計一下,最大可能會到十六萬左右,這不知道撐不撐得住...

再改回雙向紀錄其實也很快就是,就看哪種比較節省系統資源。

然後 photoships 的 compact 滿有用的,沒跑之前約衝到一兩萬筆,
每人只留三筆後就只剩三百多。想想,這數量可是會遠遠超過相片量!
假設每個人有 100 張,共有 100 人,總數就是一萬張。
此時每個人假設有 10 個朋友,每個人每個朋友都會保留三張,
100 * 10 * 3 => 900
一百個朋友的話就是 9000 張了。這樣要突破一百萬筆資料,真的會很快...

啊如果我要處理這麼多東西,還要搞 UI 和 javascript, 實在是很煩 :s

為什麼寫這麼久!?因為事情真的是好多啊...
剛剛稍微翻了一下程式碼,也想起好多東西都是之前花很多心思弄的。
不過那些東西真的是寫好之後就可以忘記,因為後面用起來都很順手。
例如 @visitor 就一定是現在這個拜訪者,而 visitor_open 就一定有權限管控。
each_photos 就可以 travel 剛剛上傳的所有 photos,
massive_update 就可以邊檢查權限邊更新所有 photos.

一層一層這樣包上來,隨時在各個層次穿梭著。

0 retries:

Post a Comment

All texts are licensed under CC Attribution 3.0