What have you found for these years?

2011-05-31

rest-core (1)

前篇提到的問題,大抵上是差不多解決了,雖然用了點怪招,再加上有個
地方我不是很滿意。但這個不滿意的地方,目前沒有任何想法可以改善,
因此大概就先這樣寫...

怎麼說,雖然還沒到非常滿意,不過還算是滿順利的。

logfail 的部份,用同樣的方式解決,就是在 env 裡面塞 'core.log'
'core.fail'. 這兩個東西都必須是 array. 想要 log 的 event 或是想要報錯的
東西,都往這兩個 array 裡面塞就對了。

CommonLogger'core.log' 裡面的東西取出,然後產生 log output.
Cache 自己去 'core.fail' 裡面看看有沒有東西,有東西則不要 cache.

這是 CommonLoggercall:
def call env
  start_time = Time.now
  response = app.call(flush(env))
  flush(log(response, Event::Requested.new(Time.now - start_time,
                                           request_uri(response))))
end
應該是還滿直覺的。有一個重點是,原本 Rack 的 call 的 type 是 Env -> Response,
而 Env 是 Hash, 另 Response 是 [Status, Headers, Body#each]
Body#each 我想表達是某個 respond_to?(:each) 的 type.

我本來是要讓 rest-core 的 call 是 Env -> String, 不過真的很彆手彆腳,
因此就決定弄成 Env -> Env... 在最末層的 app 裡,就是讓 env 去 merge
RESPONSE_BODY 和 RESPONSE_HEADERS. 這樣就能夠帶出很多
可能會用到的資訊。也因此,變成可以寫類似這樣的東西:
app.call(app.call(env))
乍看之下不知道在幹嘛沒什麼意義,但依照這方式寫下去,很多地方就變得
很方便。如上面的 CommonLogger#call, 我覺得看起來還滿 monadic 的...

而這就是 ErrorDetectorcall,
就是說呢,有 error 時,將這個 error 塞進 env (response) 裡面。
def call env
  response = app.call(env)
  if response[RESPONSE_BODY].kind_of?(Hash) &&
     error = error_detector(env).call(response)

    fail(response, error)
  else
    response
  end
end
那個 fail 就單單只是把 error merge 進 response 中。

不過很不幸的是,事情並沒有這樣就結束。Cache 比他們都要來得複雜 :(
問題在於,Cache 和其他 middlewares 形成某種 circular dependency...

* * *

在探討這個 circular dependency 之前,先來看看另一個上篇提到的問題,
是怎麼解決的。上篇提到 RestGraph#url 的實作,需要利用所有的
middleware 把 request_uri 建立起來,但卻不要真的跑去 request.
加入 DRY_CALL 太容易造成問題了。

解決方法非常單純,今天早上的捷運上忽然想到的 :o
想到這個沒花幾分鐘,實作這個也沒幾分鐘。果然在在證明,
卡住的時候不要硬幹,起來走走,去哪個地方閒晃旅遊一下 XD
問題就會在一閃之中解決...

我是這樣想的。其實我只要置換 RestClient 這個 app 即可。因為真正唯一
需要忽略的,確實只有他。取而代之,送他一個 Ask app, 也就是:
class RestCore::Ask
  include RestCore::Middleware
  def call env
    env
  end
end
嘿嘿,是不是很像 reader monad 的 ask, 正是 id? (誤)
其實比較像 state monad... 所以應該叫 get 嗎? (思)

* * *

回到 Cache 的 circular dependency, 事情是這樣的。ErrorDetector
要 detect error 之前,必須先 decode json. 這裡我們得到
JsonDecode -> ErrorDetector.

接著,我並不希望 Cache 存入 decode 之後的結果,我希望存入原始
response string, 原因有二,一是存入 decode 後的結果意味著需要
serialization 而不是 binary string, 這樣又有其他的 dependency.
二是如果想抽換 decoder 的話,或是說如果忽然想用其他方式去 parse
response 的話怎麼辦?所以希望盡量存入原始的資訊,而不是處理好後的
資訊。這裡,就得到 Cache -> JsonDecode, 就是說從 Cache 讀出
資料後,要再 decode (parse) 一次結果。

串上上面得到的東西,得到:

Cache -> JsonDecode -> ErrorDetector

問題來了。如果 ErrorDetector 偵測到錯誤,則不要 cache. 這個順序是
ErrorDetector -> Cache, 和上面的打架了 :(

我是把這部份寫完才發現這個問題的... 要嘛變成錯誤也 cache 了,
要嘛變成整個 decode 結果 cache 住了。

為了解決這個,我另外加入個工具叫 Wrapper, 也就是讓 Cache 本身也能夠
使用 middleware! 因為仔細看看上面的結果,其實可以靠這樣解決:

Cache -> JsonDecode -> ErrorDetector -> Cache

也就是說,Cache 本身是包覆著 JsonDecode 和 ErrorDetector,
而這也正是 middleware 本身的優勢:可以在 app 前後做一些動作。
因此,Cache 本身應該也要能使用 middleware 才是。
實際寫起來變成這樣:
use Cache         , {} do
  use ErrorHandler  , lambda{ |env| raise ::RestGraph::Error.call(env) }
  use ErrorDetector , lambda{ |env| env[RESPONSE_BODY]['error'] ||
                                    env[RESPONSE_BODY]['error_code'] }
  use JsonDecode    , true
  run Ask
end
這邊又看到 Ask 了... 因為 Cache 只是把 wrapped app 當做一個 function,
並沒有真的需要去 request 某個 REST API. 為了讓這邊的架構跟 middleware
完全相同,因此需要放這個假的東西進去。實際上 Cache#call 則是:
def call env
  if cached = cache_get(env)
    wrapped.call(cached)
  else
    response         = app.call(env)
    response_wrapped = wrapped.call(response)
    cache_for(env, response) if (response_wrapped[FAIL] || []).empty?
    response_wrapped
  end
end
可以看出,如果有 cache 結果,讓 wrapped app 重新調整 response,
這邊就是 decode json. 同時,預設的 wrapped app 是 Ask, 所以就算
沒有用任何 middleware (JsonDecode), 也是沒有問題的。

如果 decoded 的結果看出有 'core.fail' (FAIL) 在裡面,則不要 cache,
反之 cache 原始 response, 而不是 decoded response.

如此一來,就不需要在偵測到錯誤時,趕緊把 cache 刪除,也不需要使用
exception, 也不需要加入 call 以外的 interface 了。

* * *

到這裡都還很滿意,死是死在 Struct 的部份。在建立 client 時,會先把
所有的 middleware 都抓出來,把裡面的 attributes 也同時在 client 裡
建立一份。這使得 middleware 初始化裡面的值可以當預設,同時每個
client 也可以將此次 request 所需要的特例設定傳進去。

比方說,預設是要 json_decode, 但實際上可以這樣去調整:
RestGraph.new(:json_decode => false).get('spellbook')
或是
RestGraph.new.get('spellbook', {}, {:json_decode => false})
也就是說,所有 middleware 的 attributes, 都是 client (RestGraph) 的
attributes. 我不知道這樣是否很混淆,但我用起來是覺得很方便。

現在問題在於,JsonDecode 這些 middleware 被 Cache 包起來,
RestGraph 本身無法直接看到了!這使得這些被 Cache 包起來的
middleware 的 attributes 沒辦法被直接抓出來。我本來以為可以,
但事實上由於「用了哪些 middleware」這件事是在 block 裡,
也就是說他必須實際上去 evaluate 這個 block 才能得知有哪些
attributes, 而不幸的是,在產生 Struct 的過程中,並不會產生
middleware 的實體,因此也就不會去 evaluate 這個 block,
就無法得知這個 Cache 裡面究竟用了哪些 middlewares......

目前想不到任何解,因為這是設計上的問題,而除了這個地方以外,
感覺都運作良好,因此我只想想辦法繞過這個問題... 笨方法就是故意
產生一個沒有意義的 Cache instance, 藉此 evaluate block, 藉此
得到所有 Cache 的 middleware 並抓出 attributes, 然後捨棄這個
沒有意義的 Cache instance...
# TODO: this is hacky... try to avoid calling new!
middle.new(Ask.new, *args, &block).members
這邊其實也跟 compile time / runtime 有關。依照 Wrapper 的設計,
其實是可以動態改變 middleware 的!而這邊這樣寫,則會使得
attributes 本身是無法動態改變的。但正常來說應該不會想去動態
改變 middleware, 因此可以假定「正常使用下」並不會碰到問題。

簡單地說,這是設計上的缺陷 :o... Struct 是靜態的,Wrapper 是
動態的,卻必須從動態的 Wrapper 中取得靜態的 Struct 資訊。
而我們其實並不需要動態的 Wrapper! 因此理想的設計是,讓 Wrapper
也變成靜態的,這樣就能夠用正常的方式取得靜態的 Struct 資訊。

看看以後會不會再靈光一閃想到怎麼改善這部份...


說真的,雖然一方面我不太會解釋,程式碼也不長,但我認真懷疑
誰會有興趣與耐心來搞懂這其中的細節...

0 retries:

Post a Comment

All texts are licensed under CC Attribution 3.0