Sunday, March 22, 2015

西班牙葡萄牙我們來惹 (8) Lisbon 蛋塔的故鄉里斯本, 貝倫區

出發前小大問我從西班牙到葡萄牙這段路介不介意坐夜車
我毫不猶豫的說沒問題(主因是我直覺認定是坐火車, 但買票前我才赫然發現其實是坐巴士XDD)
那夜凌晨,我們坐上了Sevilla開往Lisbon的紅VAN的巴士(連電影名稱都偷來用是怎樣)
默默期待能佔兩個位子睡得舒服些, 但泡泡一下就被戳破, 出發時整車載滿滿啊~
一路上睡睡醒醒又睡睡醒醒, 不是太舒服但我撐過來了(是在驕傲什麼)!!
清晨五點多天還沒亮, 我們到達巴士終站, 不過到的時間太早地鐵還沒開吶~
昏昏沈沈的等到六點的第一班車, 要到市中心今晚住宿的旅館寄放行李

一切都安頓妥當後我們坐公車到貝倫區(Belém)
要完成在葡萄牙最重要的一件事(小大可能不見得同意XD)吃到Pastéis de Belém的蛋塔~
鄰近的修道院(Mosteiro dos Jerónimos)是蛋塔誕生的地方,
而Pastéis de Belém一直到今天都遵照古老的修道院食譜製作蛋塔哦!

兩隻早鳥不用排隊, 熱騰騰剛出爐的蛋塔真是超好吃, 酥皮酥得不可思議!
另外也點了同是酥皮褂的可頌夾火腿起司, 也是好吃~
廚房裡有好多蛋塔等著被吃掉(舞動)

填飽肚皮之後就開啓觀光模式啦! 可惜一早天氣就陰陰的Q.Q

往修道院的方向走, 先看到Lisbon的地標之一發現者紀念碑(Padrão dos Descobrimentos)
建造成船型的就是為了要紀念葡萄牙威風的航海時代

船上有很多人但我不知道他們是誰XD


天色一直霧霧的還飄著小雨, 我們就沒有多停留, 岸邊有很多人在釣魚.

路上看到了雙胞羊~

另外一個著名的地標是貝倫塔(Torre de Belém)
它也是航海時代的建築, 已被列為世界遺產囉 除了具有紀念意義外也因視野遼闊具有軍事上防禦的功能!


接著我們前往另一個世界遺產熱諾尼莫修道院(Mosteiro dos Jerónimos)
門窗柱子的華麗浮雕非常驚人!
長得很酷但我拍的很爛XD

寬廣的中庭, 在這裡停留了很久, 因為這裡的柱子跟牆壁上有好多趣味的浮雕!

這隻晃神的動物是獅子嗎!? 我覺得我們長得還蠻像的XD

這幾隻也蠻俏皮的

另外還有歌德式風格的教堂,

離開貝倫區之前我們又到蛋塔店外帶兩個稍候吃
涼掉了還是很好吃哩


小大版: 葡萄牙正宗葡式蛋塔在Lisboa

番茄大蒜香草烤雞腿

這食譜已經做了兩三次了!
只要把材料洗洗切切擺好之後送進烤箱就能等著吃囉~
可以參考廚房裡的人類學家莊祖宜的示範.


不過雖然簡單歸簡單還是有幾個小地方要注意,
雞腿一定要先擺到烤盤裡,
先放番茄容易讓雞腿底部無法泡在後來烤出來的雞汁番茄湯汁裡
雞腿會比較乾而且會沒味道!
另外就是材料不要堆到雞皮上面, 這樣皮會烤不脆.

還有香草我用的是百里香, 特別喜歡它清新的香氣啊~

最後呈盤長這樣, 馬鈴薯是蒸完在用平底鍋煎過會脆脆低
噢! 我是用去骨雞腿所以它看起來扁扁的比較沒精神啊哈哈

Friday, February 27, 2015

TrivialDrive: Sample for Google Play In-app Purchase

萬萬沒想寫了iOS in app purchase之後四年, 我會寫Google Play in app purchase, 真是造化弄人(又亂用成語)啊~~

總之就是利用官方文件提到的TrivialDrive範例跑完整個流程囉!
不囉唆直接開始

[在Google Play設定要賣的東西]
1. 申請Google Play開發者帳號
2. 設定Google Wallet帳號
3. 在Google Play開發者網站新增app
4. 產生public key
5. 上傳用release key簽章的apk
6. 發佈app到alpha或beta測試
7. 新增in app商品, 可以是一次性或能多次購買的產品, 亦或是訂閱服務
8. 產生Google group並加入之後要用來購買東西的測試帳號
9. 設定Google group為alpha或beta測試群組


細節有點繁瑣, 有幾點要特別注意.
第一是雖然你的測試app只是要進到alpha或beta測試, 一般人沒有機會接觸到, 但還是要注意著作權的問題啊!
發佈app的時需要提供正確格式的截圖跟圖示, 我偷懶直接搜尋對的解析度的圖片就拿來用.
結果設定完成快樂回家, 隔天來公司就發現app被停權了, 只好砍掉重練Q.Q
另外就是一定要發佈app之後才能測試自己上架的商品,
若還只是draft僅能測Google提供的商品, 能分別針對不同的購買情境.
Note: Previously you could test an app by uploading an unpublished "draft" version. This functionality is no longer supported. However, you can test your app with static responses even before you upload it to the Google Play store. For more information, see Draft Apps are No Longer Supported.



[從app購買商品]
1. 下載TrivialDrive範例程式, 如果是用Andriod Studio,只要有抓Google Play Service SDK就可以直接匯入
2. 更新package的名稱 (用IDE提供的refactor->rename很快)
3. 更新public key (String base64EncodedPublicKey = ...)
4. 把商品換成在play developer裡面新增的product id
5. 建置並用把app用release key簽章
6. 把apk放上google play(對應到前一段的5&6)
7. 把apk裝到手機上(不能用emulator測哦)

進到app會看到這個畫面, 大概就是模擬開車, 開車會消耗油, 油不夠就要買等等

這個畫面是點選Upgrade My Card(對應到我的一次性購買商品)之後, 會出現商品說明及價格等資訊

之後可能會需要輸入密碼才能繼續購買, 然後就成功囉~

另外一個是點Get Infinite Gas(對應到我的訂閱型態的商品)會出現的畫面,
因為我設定了七天的試用期, 所以一開始不會馬上收費


從Google Wallet的記錄可以看出來, 一直到七天以後才正式付款哦



[查詢購買狀態]
在我們提供的產品或是服務需要由server提供的情況下,
server需要有辦法能夠確認使用者的購買紀錄,
這時Google Play API就派上用場啦!

1. 申請Google developer帳號
2. 開啟新的api專案
3. APIs&auth選單: 先到API設定開啓Google Play Android Developer API
4. APIs&auth選單: 新增Client ID, 並選擇web application為應用類型, 填妥必要資訊
(也可選擇用Service Account但我卡關XD)
5. 回到Google Play的API access設定頁面, 將剛剛的api專案連結到你的帳號
6. api帳號利用OAuth取得google play帳號的權限
(拿到authorization code之後, 就可以讓server用它換到refresh token然後就無敵惹XD)
7. 查查查

OAuth的url大概是長這樣, 重點是scope還有要能offline access
https://accounts.google.com/o/oauth2/auth?scope=https://www.googleapis.com/auth/androidpublisher&response_type=code&access_type=offline&redirect_uri=https%3A%2F%2Fwww.rita.inapp.com%2Foauth2callback&client_id=00000000000-xxxx.apps.googleusercontent.com

api的用法可以直接參考官方文件,
其他需要注意的是:
針對同一個app, Google Play API一天可以打得次數是有上限的, 所以最好只在必要的時候用.
(而且我們也不希望因為request量太大影響整體系統效能吧)
官方文件的建議是希望我們能把查過的訂購資料記下來,
如果是單次性購買的產品, 就只要在使用者剛買完的時候查過一次就夠了,
訂閱型的產品則是在訂閱到期的時候重新查詢狀態.

ref:
- Google Play API
- Google Play In-app Billing

Wednesday, February 25, 2015

探索Playframework: 如何正確使用thread pool

在scala的世界, 我們常會用Future來處理某些非同步的工作,
用Future, 必需要透過implict的方式提供execution context, 來執行這些工作.

白話的說就是要指定你的code要在哪個thread pool跑.

使用Playframework, 在比較單純的情況下直接用預設的thread pool就可以了.
然後再透過application.conf設定適當的參數.

import play.api.libs.concurrent.Execution.Implicits._

def someAsyncAction = Action.async {
  import play.api.Play.current
  WS.url("http://www.playframework.com").get().map { response =>
    // This code block is executed in the imported default execution context
    // which happens to be the same thread pool in which the outer block of
    // code in this action will be executed.
    Results.Ok("The response code was " + response.status)
  }
}

play {
  akka {
    akka.loggers = ["akka.event.Logging$DefaultLogger", "akka.event.slf4j.Slf4jLogger"]
    loglevel = WARNING
    actor {
      default-dispatcher = {
        fork-join-executor {
          parallelism-factor = 1.0
          parallelism-max = 24
        }
      }
    }
  }
}

import play.api.libs.concurrent.Execution.Implicits._
先來找找我們用的execution context在哪裡,
defaultContext是ActorSystem的dispatcher
而create ActorSystem用的設定是"play"這個關鍵字下面的所有屬性.
接下來就是看ActorSystem如何設定dispatcher囉~
defaultGlobalDispatcher是用DefaultDispatcherId(即akka.actor.default-dispatcher)抓到設定值

大概是這樣囉!


針對不同類型的工作, 有時候我們會不希望所有事情都塞在預設的thread pool做,
要使用其他的thread pool, 只需要先在application.conf加上一組新的設定.
再用lookup讓Akka能找到它就好.
可以直接在產生Future的時候指定execution context或是直接import讓implicit發生作用
my-context {
  fork-join-executor {
    parallelism-factor = 20.0
    parallelism-max = 200
  }
}

object Contexts {
  implicit val myExecutionContext: ExecutionContext = Akka.system.dispatchers.lookup("my-context")
}

Future {
  // Some blocking or expensive code here
}(Contexts.myExecutionContext)

or

import Contexts.myExecutionContext
Future {
  // Some blocking or expensive code here
}

當然實際上要如何設定thread pool還是要參考測試結果啦!

ref:
- playframework source code (2.3.x)
- akka source code (2.3)
- understanding play thread pools (2.3.5)

Monday, January 19, 2015

水林泰平花生醬: 涼拌小黃瓜雞絲

在厚生市集買了好幾次菜囉!
對水林泰平花生醬感到十分好奇, 新鮮現磨的手工花生醬到底有多厲害勒!?
上禮拜為了要湊到免運費就順手定了.

到貨後立馬烤了片土司, 開罐後花生醬真是超香的(暈眩)
花生醬本身沒加糖, 我就直接撒了點糖(細粒冰糖)上去
相當好吃, 但是我上次吃到花生醬已經不知道是多久前了, 無從比較啊XD


之後發現它的保存期限相當短
於是就驚慌的搜尋起有用到花生醬的食譜
發現小小米桶的麻將棒棒雞絲看起來相當誘人, 就來試試囉!


完全照著做就挺成功的.
為了要快速消耗花生醬我完全沒用芝麻醬(其實家裡也沒有啦XD)
雞絲有特別撕大塊一點吃起來比較過癮
感覺雞絲有點乾乾的就把它泡在蒸出來的雞汁裡放涼覺得效果還不錯
而且老乾媽辣椒的確是有必要, 畫龍點睛啊!


剩下的醬汁用醬油跟醋稀釋(也加強鹹度酸度)後就拿來拌麵也很好吃哦!

Tuesday, January 13, 2015

Snowboard再體驗, 迷人的小村莊 - 野澤溫泉, 開滑囉! 大雪下不停~

嘗試在今年開滑之前把去年欠的補完啊哈哈XD

抵達野澤的隔天終於要開滑了(舞動)
一早就下著毛毛雪(有這種用法嗎!!?)
吃完早餐集合完畢剛要出門就看到已經有早起鳥兒滑完收工了~

走啊走到了終於到了電扶梯的起點,
沒圖沒真相XD
很類似香港中環到半山的電扶梯的感覺, 不過不是階梯的
經過了不算短的距離之後終於到達雪場囉!
我們先到裝備出租店面搞定雪板鞋子等等東西.
再來熱身完畢就開滑囉!
早上我們都待在日影(HIKAGE)滑道,
馬克教練帶著大家滑瞭解我們的程度同時糾正跑掉的動作,
時隔一年連下纜車都好緊張! 深怕會跌倒害纜車停住那超丟臉搭.

雪越下越大,

中午我們就在雪場入口附近的餐廳吃吃,
毫不猶豫就選擇了摩斯XD

下午我們搭日影Gondola滑天堂(PARADISE)滑道
超大雪所以也沒拍照了
總之就是持續練習S-turn為明天的SKYLINE作準備啦!


用晚餐照結尾
這是一開始上桌的畫面之後又來的炸物等等東西吃好飽啊!


[野澤系列]
- Snowboard再體驗, 迷人的小村莊 - 野澤溫泉

Sunday, January 11, 2015

LADURÉE百年糕點老舖的傳奇配方: 焦糖布丁


身為布丁控, 焦糖布丁是一直我很想嘗試自己做的甜點,
但都處在萬事俱備只欠東風(明明就只是懶)的狀態.
在2014年的最後一天晚上,
發現好不容易搶到的初鹿鮮乳再幾個小時就要過期.
就憑著一股衝勁開工囉!


主要是參考松露玫瑰部落格上貼的LADURÉE食譜.

照著做大致上算是挺順利, 只有幾個步驟小小卡關,
一開始取香草籽放到牛奶鮮奶油混合液稍微加熱後,
香草籽始終無法均勻的分散開來, 造成最後的成品香草籽顆粒太大不好看.
不知道這步驟有沒有什麼訣竅.
另外是不太明白做焦糖最後鍋子離火放到冷水中,
焦糖加熱水攪拌是為了什麼.
我加熱水完全攪不動 囧
只好再放回爐火上重新回到焦糖狀態,
但這次就直接把焦糖倒入小容器中冷卻了, 參不透其中的奧祕啊~

這個食譜做出來的焦糖布丁超厲害啊!
布丁非常的綿密柔滑, 吃第一口的時候我整個人有點嚇到.

最後的叮嚀就是, 把布丁倒出來的時候不要偷懶啊! 用的刀要夠利夠薄.
要不然邊邊就會跟下面這張圖一樣像是狗啃搭~

Tuesday, January 06, 2015

Amazon DynamoDB - Global Secondary Index (GSI)

[什麼是GSI]
Global Secondary Index跟Local Secondary Index很類似,
都是讓你可以在原有的table上選擇另一組key來幫助資料查詢.
不過GSI的彈性更大, Hash key可以是任意的attribute, 也可以跟原本的table的schema不同.
LSI只能用在原本是Hash-Range key的table,
GSI則是Hash/Hash-Range key都可以, 其所建立的index也不一定要有Range key.
但還是只能選擇單一值的attribute當key.

[對throughput的影響]
與LSI不同, GSI並不會佔用原本table的throughput,
而是需要另外指定專屬於那個index的throughput.
因為我們並不會直接寫入資料到GSI, 而是在我們新增修改刪除原始table的時候,由DynamoDB主動更新GSI的資料.
所以就是用更新的頻率來預估計寫入的throughput,
讀取的throughput就是看你的使用情境
另外, 讀取GSI的資料只能是eventually consistent哦!

ref:
- Global Secondary Index

Amazon DynamoDB:
- Local Secondary Index

Monday, January 05, 2015

Amazon DynamoDB - Local Secondary Index (LSI)

在某些情況下, 我們所定義的(Hash/Hash-Range)key並不能滿足所有查詢資料的使用情境,
這時候我們可能會利用Query加上特定的filter,或是利用Scan來找到符合的資料,
但這兩種方法不僅沒有效率(不能保證幾次來回才能拿到所有結果)我們也難以掌握需要消耗多少throughput
我們也可以用額外的table記錄相同的資料但選擇不同的key來滿足各種查詢情境,
但是要維持每個寫入都要完整更新到各個table並不是件容易的事.

為了解決這個問題, DynamoDB提供了Local Secondary Index(LSI), 其實已經推出好一段時間囉XD

[什麼是LSI]
在原本的Hash-Range key之外, 能夠利用原本的Hash key搭配其他的attribute作為新的Hash-Range key來幫助各種可能發生的查詢.
- 選擇的attribute必須是單一值不能是集合
- 此attribute的值不需要是唯一
- 寫入還是針對原本的Hash-Range key, DynamoDB會根據設定更新LSI的資料

[注意事項]
- 一個table最多只能設定五組LSI
- 可以選擇哪些其他的attribute要一起被複製進LSI(原本的Range key一定會被加進去)
- 同一個Hash key底下最多只能掛10GB的資料量, 所以得要在便利性以及資料大小間取得平衡

[對throughput的影響]
- 對LSI的讀寫是消耗整個table的throughput
- 除了10GB的限制會讓我們需要取捨要複製哪些attribute進LSI, 另外要考慮的就是資料越大讀寫時需要的throughput就越大啦
- 如果寫入會影響到LSI的資料, 則會需要消耗額外的throughput:新增,更新(刪除再新增),刪除


ref:
- Local Secondary Indexes

Sunday, December 07, 2014

Amazon DynamoDB - Data Modeling and Scaling Best Practices | AWS re:Invent 2014

前情提要:
Amazon DynamoDB會將一個table切成好幾個partition,
而整個table的throughput會平均分配給各個partition.
Total provisioned throughput/partitions = throughput per partition
不過partition跟思念一樣都是很玄的東西,
Amazon DynamoDB的官方文件只輕描淡寫的說是依照hash key來決定item是屬於哪個partition,
我們無從得知我們的table實際上被切成幾個partition.
然而在今年的AWS re:Invent, Amazon DynamoDB: Data Modeling and Scaling Best Practices的講者終於揭開了partition的奧秘!
剛剛查了一下官方文件不知何時也更新囉!
Partition的個數跟table的資料量(大小)以及讀寫的provision throughput有關.
若是你的RCU和WCU都是1000,
( 1000 / 3000 ) + ( 1000 / 1000 ) = 1.333
那你的table就會需要兩個partition, 分別有500的RCU和WCU.

依照公式可以看出,
若是我們的資料量不大而且需要的讀寫量(單筆資料&頻率)不高,
其實table是不會被切成多個partition哦!.
所以不用擔心存取資料(key)不夠分散, 某些狀況下可以好好利用這點.

另外講者也提到一些實際應用上的小技巧,
針對熱門的read item像是拍賣主打的商品資料要cache來降低花費(減少需要的throughput)
針對熱門的write item像是投票系統的候選人則是要自行把候選人切成多個key讓寫入能夠分散
(投票的人數多過於候選人, 且票數常常是集中在固定幾個人身上)

不錯的影片, 有在用DynamoDB的人值得看看啦!

Sunday, July 13, 2014

西班牙葡萄牙我們來惹 (7) Sevilla城堡

自從看到HBO發佈Game of Thrones第五季將會有部分場景在西班牙拍攝的消息就感到非常興奮,
官方消息不僅透露西班牙將會作為馬泰爾家族的根據地冬恩,
更明確的指出Alcázar of Sevilla(Sevilla城堡)將會是水之花園的拍攝場所,
雖然這一系列的遊記已經拖稿快兩年, 聽到此一消息讓我又重獲動力啊!

Alcázar of Seville在旅遊書上的譯名相當混亂, 有的稱之為賽維亞皇宮, 也有的叫它做阿卡乍堡.
其實Alcázar就是城堡的意思, 而且它一開始是由摩爾人建造而成的城堡,
後來的改建及擴建則又受到文藝復興時期的藝術風格和新古典主義的影響.

我們由獅子門進入,

走

進門


摩爾人的建築真的很厲害, 光是看從外頭看城牆會覺得整個城堡氣勢恢弘,
主建物本身卻又不同, 格局是一個封閉的長方形, 有著許多許多房間,
拱廊圍繞著庭院, 庭院中間會有個水池, 感覺就是個能讓人心情放鬆的地方,
然而他們不僅考慮到機能性, 建築的裝飾上也充滿著各種不同的元素,
細節精美的程度讓人感到相當驚豔!
拱廊有著繁複的雕刻,



屋頂的金色裝飾讓人目眩神迷!


牆壁上也貼滿了瓷磚, 相同樣式的瓷磚重複排列形成了如萬花筒般的複雜變化,
瓷磚的圖案有基本的幾何形狀也有各種植物動物,
這個人面魚尾真是超可愛啊!


往上走到二樓,

在這裡甜小大發現只要戴上墨鏡我拍照就無敵了(是嗎XD)

隨後我們到了後方的花園閒晃,


離開城堡之後我們就展開充實的逛街行程啦!

小大版: 西班牙也有火龍果 Cactus Figs in Seville(有早午餐跟晚餐)

ref: Alcázar of Sevilla

西班牙葡萄牙我們來惹 (2) 太陽海岸 Costa del Sol: Nerja, Frigiliana, San Pedro
西班牙葡萄牙我們來惹 (3) 藍色小精靈村Juzcar以及鬥牛的故鄉Ronda
西班牙葡萄牙我們來惹 (4) 快閃直布羅陀Gibraltar以及雪莉酒的故鄉Jerez
西班牙葡萄牙我們來惹 (5) Sevilla主教堂, 希拉達塔, 西班牙廣場

Monday, June 30, 2014

呱呱蜜汁鴨胸


之前跟孔雀一起買了豪野鴨, 因為是難得的合購所以一口氣就買了四片鴨胸XD
本來想說可以多試試幾種煮法,
但是參考了蘿瑞娜的三步驟搞定懶人版蜜汁燒鴨食譜之後,就懶得再找別的來試了XD
因為這個版本真的是簡單又好吃啊

[主材料]
- 豪野鴨特選鴨胸 *1塊約300g
- 青蔥 *1支
[醬汁]
- 李錦記蜜汁烤肉醬
- 李錦記蠔油
- 米酒
- 水
- 蜂蜜

[步驟]
1. 鴨胸用米酒加鹽巴醃半小時
2. 鴨胸先擦乾, 皮朝下先用大火煎3分鐘轉中火煎7分鐘
3. 翻面煎8分鐘
* 煎鴨胸很會噴油, 最好用深一點的鍋子事後才不會難清理
* 這個時間我覺得蠻剛好的, 我只有第一次做的時候比較緊張從頭監控到尾
* 之後就都放著煎然後用手機定時就去做別的事情了
4. 翻面煎的時候另一個平底鍋煮醬汁, 比例可以參考蘿瑞娜的部落格
* 我自己是不太喜歡蜂蜜煮熟的味道, 後幾次做就都沒放了
* 比例的話我是蠻隨性的, 因為跟鴨肉一起煮的時間沒有很長, 基本上味道不會太重
* 如果真的太淡吃的時候就多沾點醬, 太鹹就多配點蔥囉XD
5. 翻面煎完將鴨胸放到醬汁鍋小火煮5-8分鐘
6. 之後關火放在鍋中休息5分鐘後切片, 跟牛排煎完要休息之後才能切片避免肉汁跑掉的道理應該是一樣的吧!
7. 蔥切絲, 將剩餘醬汁呈到小碟子後就可以擺盤啦!

Sunday, June 29, 2014

Gatling筆記: 完成自己的simulation

[基本單元]
1. scenaio: 描述使用情境, 一個simulation可以指定多個scenario
2. exec: 指定要做的事, 通常會是http request, 一個scenario裡面用exec串接多個動作
3. http: 設定request格式, 也可加入response檢查
4. httpConfig: 設定整個scenario共用的protocol(要打哪個domain, 要不要自動redirect等等)
5. users: 要用幾個user來跑
6. ramp: 設定每個user一開始起來跑的間隔
scn.users(10).ramp(10) // 10 users/10s = 1 user/s
scn.users(10).ramp(20) // 10 users/20s = 0.5 user/s = 1 user every 2s
scn.users(1000).ramp(100) // 1000 users/100s = 10 users/s

[HTTP]
1. GET: 以下是基本的HTTP GET範例
val httpConf = httpConfig.baseURL("http://my.website.tld")

val scn = scenario("My Scenario")
  .exec(
    http("My Request")
    .get("/my_path") // Will actually make a request on "http://my.website.tld/my_path"
  )
  .exec(
    http("My Other Request")
    .get("http://other.website.tld") // Will make a request on "http://other.website.tld"
  ...

setUp(scn.protocolConfig(httpConf)...)
2. 帶query string:
- 直接串在uri上
- queryParam(key: String, value: String)

3. 加header:
- 加一個: header(key: String, value: String)
- 加多個: header(Map[String, String]())

4.帶request body:
- 直接給字串: body(body: String)
- 從檔案讀: fileBody(fileName: String)
** 要注意檔案要放在galting目錄的這個路徑底下user-files/request-bodies/
- 從檔案讀且檔案內有變數可以替換: fileBody(templateFileName: String, valuesToReplace: Map[String, String])
** 同樣檔案要放在galting目錄的user-files/request-bodies/路徑底下, 且副檔名須為ssp
- 給byte array: byteArrayBody (byteArray : (Session) => Array[Byte])
- 另外還有POST特有的form以及multi-part格式

[Session]
session是一個Map, key為String, value可以是任意類型,
每個user在執行scenario時都有一個專屬的session可以暫存各種之後會用到的東西
1. 儲存:
- saveAs("myKey"), 通常是與Check合在一起用
- setAttribute("myKey", "myValue")
2. 取出:
- "${myKey}"
3. 如果對語法不熟悉或是發生什麼怪狀況需要debug時, 可以在用exec特別處理session
** 因為session是不能修改的, 所以當你動到session的內容時, 它其實是產生一個新的session物件給你
- getAttribute(key: String): Any = getTypedAttribute[Any](key)
- getTypedAttribute[X](key: String)
- getAttributeAsOption[T](key: String): Option[T]
- setAttributes(attributes: Map[String, Any])
- setAttribute(attributeKey: String, attributeValue: Any)
- removeAttribute(attributeKey: String)
- isAttributeDefined(attributeKey: String)
- getCounterValue(counterName: String)
- getTimerValue(timerName: String)
.exec(session =>
  session.setAttribute("foo", "bar")
  println(session)
  session
)

[Feeder]
用來餵參數用的好東西, 通常會是讀檔但也把JDBC或Redis當作來源,
1. 讀檔怎麼讀
- csv( filename: String ), 每個值是用,分隔
- ssv( filename: String ), 每個值是用;分隔
- tsv( filename: String ), 每個值是用TAB分隔
- 自訂分隔符號csv("user_credentials.csv", '#')
2. 每次呼叫feeder時, 會從來源取得一筆資料, 有幾種模式
- queue(預設): 照順序讀進來, 讀到底之後simulation會終止, 使用這個模式的時候要注意給的資料要夠多
- random: 隨機
- circular: 照順序讀進來, 讀到底之後會從頭讀
3. 如何存取
資料被feeder讀進來之後會被放在session裡, 如果是從檔案讀的話, key就是對應到檔案第一行的名稱
username,password
john,smith21
john,doe43
4. 使用自己的feeder
Feeder其實是一個簡單的class, 當你呼叫它的提供一個next函式時會回傳一個Map.
package object myfeeders {
  import com.excilys.ebi.gatling.core.feeder._

  val myAccountFeeder = new Feeder[String] {
    import org.joda.time.DateTime
    import scala.util.Random

    private val RNG = new Random

    // random number in between [a...b]
    private def randInt(a:Int, b:Int) = RNG.nextInt(b-a) + a

    private def daysOfMonth(year:Int, month:Int) = new DateTime(year, month, 1, 0, 0, 0, 000).dayOfMonth.getMaximumValue

    // always return true as this feeder can be polled infinitively
    override def hasNext = true

    override def next: Map[String, String] = {
      val email = scala.math.abs(java.util.UUID.randomUUID.getMostSignificantBits) + "_gatling@dontsend.com"
      val year = randInt(1945, 1994)
      val month = randInt(1, 12)
      val day = randInt(1, daysOfMonth(year, month))

      Map("contactEmail" -> email, 
        "birthdayYear" -> year.toString, 
        "birthdayMonth" -> month.toString, 
        "birthdayDay" -> day.toString)
    }
  }
}

[Check]
就是可以讓你檢查response是否與你預期相同,
檢查的範圍除了http status,還可以檢查payload格式是否正確, 甚至可以抓特定的值出來確認
1. status
- check(status.is(200))
- check(status.not(404), status.not(500)))
2. body
- check(regex("""..."""))
- check(xpath(".."))
- check(jsonPath(""))
3. 另外可以用find把你想要的值抓出來, 是需求決定是否要套上transform將值做些轉換或是用saveAs把它存在session裡

[範例]
最後的最後有一些流程控制的東西懶得寫了, 就直接看範例吧!
package collection

import com.excilys.ebi.gatling.core.Predef._
import com.excilys.ebi.gatling.http.Predef._
import com.excilys.ebi.gatling.http.Headers.Names._
import akka.util.duration._
import bootstrap._
import CLTHeaders._
import myfeeders._

class MyCollectionSimulation extends Simulation {
 val httpConf = httpConfig.baseURL("http://localhost:9000/")

  val get_chain = exec(http("get collection")
          .get("api/collection/${cid}")          
          .headers(auth_headers)
          .check(status.is(200)))

  val update_chain = exec(http("update collection")
          .post("api/")
          .fileBody("Update", Map("cid" -> "${cid}", "title" -> "${title}")).asJSON
          .headers(auth_headers)
          .check(status.is(200))) 

  val my_scenario = scenario("CRUD collection")
        .during(3 minutes) {
          feed(myAccountFeeder)
          .exec(http("create collection")
            .post("api/")
            .fileBody("CreateCollection.json").asJSON
            .headers(auth_headers)
            .check(status.is(200))
            .check(jsonPath("$.collection").saveAs("collection")))
          .exec(session=> {                   
            val tmp = session.getTypedAttribute[String]("collection").split("\"")
            val cid = tmp(1)                   
            session.removeAttribute("collection").setAttribute("cid", cid)
            })
          .repeat(2){
            get_chain      
          }
          .doIf(session=>session.getTypedAttribute[String]("cid").head.toInt>80) {
            update_chain
          }
          .exec(http("delete collection")
            .post("api/")
            .fileBody("DeleteCollection", Map("cid" -> "${cid}")).asJSON
            .headers(auth_headers)
            .check(status.is(200)))
          .exec(http("get collection")
            .get("api/collection/${cid}")          
            .headers(auth_headers)
            .check(status.is(400)))
        }

  setUp(create_scenario.users(100).ramp(10).protocolConfig(httpConf))      
}

上一集: Gatling, 威力強大卻也簡單上手的Http Server壓力測試工具
ref:
- HTTP reference
- Session
- Feeders
- Checks

Friday, June 27, 2014

Scala筆記: Gatling, 威力強大卻也簡單上手的Http Server壓力測試工具

[Gatling]
Gatling的底層是Akka搭配Netty, 會比狂開thread來提升rps的方法來的有效率.
它提供了簡單的DSL就算你不懂Akka不懂Netty不懂Scala, 也可以一眼看懂範例在做什麼.
搭配文件其實蠻容易依樣畫葫蘆寫出自己的測試程式.
測試結果也有很厲害的圖表可以參考(status, rps, latency的總合及分佈等等).

[安裝]
1. 先下載Gatling的package, 我目前是用1.5.5
2. 接下來檢查你的JDK版本, Gatling跟JDK版本對應可以參考這裡, 我用Mac, JDK版本是1.7.0_45
3. 然後只要把抓下來的package解開就可以囉!


[跑跑範例]
1. 執行gatling目錄bin底下的gatling.sh會出現兩組範例可以選
Choose a simulation number:
     [0] advanced.AdvancedExampleSimulation
     [1] basic.BasicExampleSimulation
2. 開始跑之後會定期有狀態回報, 要不然就是有Waring才會顯示.
3. 如果是初學其實可以把log等級調成INFO這樣會比較有參與感XD
4. 跑完之後, 結果會以網頁的方式呈現, 可以看到以下圖表. 另外也可以看個別request的狀況, 相當方便!





ref:
- gatling github wiki page

Sunday, June 08, 2014

西雅圖逛逛 (2) Ravenna Park, Washington Park Arboretum, Kerry Park

[Beach]
離開鮭魚階梯之後, 萱小姐提議順便去鄰近的海灘晃晃
但是因為穿了長褲又沒帶拖鞋, 就只坐在椅子上遠遠的看風景沒下去玩水囉!

晴朗的天空加上寬闊的海面真是百看不厭,
但是沙灘就輸台灣很多耶XD

[Ravenna Park]
接著來到萱小姐最喜歡的公園
入口附近有個大草坪, 很多人就一派悠閒的躺著曬太陽, 遛狗或是野餐

走進去之後發現這根本就是森林啊!
充滿芬多精的感覺好健康~
走一圈大概花了快一個小時, 加上調時差一直調不好我整個人累翻了, 所以就回萱小姐家睡午覺先~


[Washington Park Arboretum]
睡飽了再出發, 但是剩下的時間已經不夠划船就直接到華盛頓大學附近的棧道走走.
但是萬萬沒想到這季節水位高, 淹過了步道沒辦法通行,
地頭蛇萱小姐就當機立斷開車到對岸植物園北邊的濕地,

對岸也是沒法走不過這裡腹地比較大可以在這兒散散步



恰巧碰上一群年輕人(我已經不是了啊TAT)歡樂出遊請我幫忙照相,
想到大學時假日幾乎都也都關在寢室看電影跟日劇,
相較之下真是差太多啦 囧

[Kerry Park]
上飛機前的最後一站來到凱莉公園看夜景
剛好有情侶在這裡結婚感覺很很溫馨,
後來又有一群小屁孩從party bus(車子上就真的是醬寫)衝下來鬼吼鬼叫
小小的公園氣氛頓時變得很混搭.


- 西雅圖逛逛 (1) Pioneer Square, Occidental Park, Hiram M. Chittenden Locks and Fish Ladder