2014/04/05

Coroutine in Tornado Web Framework

Coroutine 可以讓我們在程式中按照自己的意思去安排執行順序,有點像是 jump 的概念,它允許短暫離開 function 並且保留 local variable 的狀態,等到某個時間點再跳回來,從上一次離開的地方繼續。第一次接觸到 coroutine 的概念是從 python,coroutine 是一種語言特性,從 wiki 可以看到很多語言都有這種特性。

那我們沒事幹嘛讓程式跳來跳去的?思考一種狀況,當在 single thread 下,你執行到一個 blocking function,這時候如果讓 CPU 去做其他事情是不是很好,等到 I/O 有回應的,再跳回來原本的地方繼續執行。等等,這不就是 event-driven 的 programming 嗎?它們的表達方式還是有點區別。


舉一個例子,我們發出一個 HTTP request 去抓 Yahoo weather 的資訊,然後利用 XML parser 從回應得資料中取出溫度,以下是利用一般非同步的方式去撰寫:
from tornado.ioloop import IOLoop
from tornado.web import Application, RequestHandler, asynchronous
from tornado.httpclient import AsyncHTTPClient
from xml.dom import minidom

class MainHandler(RequestHandler):
    url = "http://weather.yahooapis.com/forecastrss?w=2306179&u=c"

    @asynchronous
    def get(self):
        http_client = AsyncHTTPClient()
        http_client.fetch(self.url, callback=self._on_fetch)

    def _on_fetch(self, response):
        degree = self._parse_xml(response.body)
        self.finish("Taipei: %d" % degree)

    def _parse_xml(self, xml):
        xml_doc = minidom.parseString(xml)
        weather_list = xml_doc.getElementsByTagName('yweather:condition')
        degree = float(weather_list[0].attributes['temp'].value)
        return degree

if __name__ == "__main__":
    application = Application([
        (r"/", MainHandler),
    ])
    application.listen(8888)
    IOLoop.instance().start()
第 12 行:在發出 request 同時指定 callback
第 14 行:在收到 server 回應後,執行 _on_fetch()


換成 coroutine 的方式
from tornado.ioloop import IOLoop
from tornado.web import Application, RequestHandler, asynchronous
from tornado.httpclient import AsyncHTTPClient
import tornado.gen as gen
from xml.dom import minidom

class MainHandler(RequestHandler):
    url = "http://weather.yahooapis.com/forecastrss?w=2306179&u=c"

    @gen.coroutine
    def get(self):
        http_client = AsyncHTTPClient()
        response = yield http_client.fetch(self.url)
        degree = self._parse_xml(response.body)
        self.finish("Taipei: %d" % degree)

    def _parse_xml(self, xml):
        xml_doc = minidom.parseString(xml)
        weather_list = xml_doc.getElementsByTagName('yweather:condition')
        degree = float(weather_list[0].attributes['temp'].value)
        return degree

if __name__ == "__main__":
    application = Application([
        (r"/", MainHandler),
    ])
    application.listen(8888)
    IOLoop.instance().start()
第 13 行:程式執行完 yield 後面的 statement 這個 function 就會立刻 return,直到 tornado io loop 收到 server 回應,然後跳回第 13 行,把 fetch() 的結果 assign 給 response,然後繼續執行下去。
底層一樣是非同步I/O,但這種表達方式擁有在寫同步 I/O 般的直覺。


單一個 callback 可能顯示不出直覺在哪裡,如果連存取 database/memcach... 任何跟I/O相關的事情都採用非同步方式,那就會需要在 callback 中執行另一個 callback
class MainHandler(RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        req1(argument1, callback=self._res1)

    @tornado.web.asynchronous
    def _res1(self, response1):
        ...do something with response
        req2(argument2, callback=self._res2)

    def _res2(self, response2):
        ...do something with response
        self.finish("result...")

改用 coroutine 的方式
class MainHandler(RequestHandler):    
    @tornado.gen.coroutine
    def get(self):
        response1 = yield req1(argument1)
        ...do something with response1
        response2 = yield req2(argument2)
        ...do something with response2
        self.finish("result...")

是不是直覺很多!

2014/03/22

C語言的記憶體洩漏(Memory Leak)偵測 - Valgrind

寫 C 的人對於記憶體管理要非常的精確,不像其他高階語言有 garbage collection,好習慣可以減少錯誤發生,讓 malloc() 與 free() 對等的出現,而且寫在同一個層級。但不免還是會有疏忽的時候,像我自己在寫 daemon,這種長期服務的程式,如果遇到 memroy leak,就會看到系統的記憶體一點一點的被吃光,最後免不了要重開的命運,如果是線上的服務器豈不就要重斷服務了!

好在有強大的 Valgrind 可以幫助我們真測出問題。


Case 1: Memory Leak

最常見的錯誤,就是 allocate 的記憶體忘記 free,而且已經沒有任何指標指著它。
我們舉個簡單的例子,並使用 valgrind 來協助偵測:
#include <stdlib.h>

void func (void)
{
    char *buff = malloc(10);
}

int main (void)
{
    func();
    return 0;
}

編譯並執行 (記得編譯時要加 -g 的參數,valgrind 的報告才會指明有問題的行數)
# gcc -g -o mem_test mem_test.c

Valgrind 使用方法很簡單,不需要修改程式碼,用 valgrind 把自己的 program 帶起來即可,如果有參數就加在後面,程式執行結束就會產生報告。
valgrind [-valgrind parameter] ./my_program [-program parameter]
# valgrind --leak-check=full ./mem_test

==59095== Memcheck, a memory error detector
==59095== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==59095== Using Valgrind-3.9.0 and LibVEX; rerun with -h for copyright info
==59095== Command: ./mem_test
==59095==
==59095==
==59095== HEAP SUMMARY:
==59095==     in use at exit: 10 bytes in 1 blocks
==59095==   total heap usage: 1 allocs, 0 frees, 10 bytes allocated
==59095==
==59095== 10 bytes in 1 blocks are definitely lost in loss record 1 of 1
==59095==    at 0x4C2C857: malloc (vg_replace_malloc.c:291)
==59095==    by 0x400505: func (mem_test.c:5)
==59095==    by 0x400514: main (mem_test.c:10)
==59095==
==59095== LEAK SUMMARY:
==59095==    definitely lost: 10 bytes in 1 blocks
==59095==    indirectly lost: 0 bytes in 0 blocks
==59095==      possibly lost: 0 bytes in 0 blocks
==59095==    still reachable: 0 bytes in 0 blocks
==59095==         suppressed: 0 bytes in 0 blocks
==59095==
==59095== For counts of detected and suppressed errors, rerun with: -v
==59095== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
報告中指明有一個 10 bytes 的 block 被偵測為 "definitely lost",位置在 mem_test.c 的第 5 行

可以看到 memory lost 分成幾種類型:
  • definitely lost: 真的 memory leak 了
  • indirectly lost: 間接的 memory leak,structure 本身發生 memory leak,而內部的 member 如果是 allocate 的出來的,一樣會 memory leak,但是只要修好前面的問題,後面的問題也會跟著修復。
  • possibly lost: allocate 一塊記憶體,並且放到指標 ptr,但事後又改變 ptr 指到這會計一體的中間 (這一點我目前也不是很清楚,建議看原文說明)
  • still reachable: 程式結束時有未釋放的記憶體,不過卻還有指標指著,通常會發生在 global 變數
就算是 library 所 malloc 的記憶體,如 evbuffer_new(),也都可以偵測的到。



Case 2: Invalid Memory Access

Invalid memory access 有時候並不會立即造成 segmentation fault,所以不會有 core dump可以查詢,需要借助像 valgrind 這類的工具來偵測。
一般情況可能是用了 allocate 的 memory 之外的地方,或是用了已經 free 的 memory,請看下面幾個例子:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main (void)
{
    // 1. Invalid write
    char *str = malloc(4);
    strcpy(str, "Brian");
    free(str);
    
    // 2. Invalid read
    int *arr = malloc(3);
    printf("%d", arr[4]);
    free(arr);
    
    // 3. Invalid read
    printf("%d", arr[0]);

    // 4. Invalid free
    free(arr);

    return 0;
}
編譯並執行
==60019== Memcheck, a memory error detector
==60019== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==60019== Using Valgrind-3.9.0 and LibVEX; rerun with -h for copyright info
==60019== Command: ./mem_test
==60019==
==60019== Invalid write of size 2
==60019==    at 0x4005AB: main (mem_test.c:9)
==60019==  Address 0x51f4044 is 0 bytes after a block of size 4 alloc'd
==60019==    at 0x4C2C857: malloc (vg_replace_malloc.c:291)
==60019==    by 0x400595: main (mem_test.c:8)
==60019==
==60019== Invalid read of size 4
==60019==    at 0x4005D1: main (mem_test.c:14)
==60019==  Address 0x51f40a0 is 13 bytes after a block of size 3 alloc'd
==60019==    at 0x4C2C857: malloc (vg_replace_malloc.c:291)
==60019==    by 0x4005C4: main (mem_test.c:13)
==60019==
==60019== Invalid read of size 4
==60019==    at 0x4005F7: main (mem_test.c:18)
==60019==  Address 0x51f4090 is 0 bytes inside a block of size 3 free'd
==60019==    at 0x4C2B75D: free (vg_replace_malloc.c:468)
==60019==    by 0x4005F2: main (mem_test.c:15)
==60019==
==60019== Invalid free() / delete / delete[] / realloc()
==60019==    at 0x4C2B75D: free (vg_replace_malloc.c:468)
==60019==    by 0x400618: main (mem_test.c:21)
==60019==  Address 0x51f4090 is 0 bytes inside a block of size 3 free'd
==60019==    at 0x4C2B75D: free (vg_replace_malloc.c:468)
==60019==    by 0x4005F2: main (mem_test.c:15)
==60019==
00==60019==
==60019== HEAP SUMMARY:
==60019==     in use at exit: 0 bytes in 0 blocks
==60019==   total heap usage: 2 allocs, 3 frees, 7 bytes allocated
==60019==
==60019== All heap blocks were freed -- no leaks are possible
==60019==
==60019== For counts of detected and suppressed errors, rerun with: -v
==60019== ERROR SUMMARY: 4 errors from 4 contexts (suppressed: 0 from 0)
錯誤一:Invalid write of size 2,試圖寫入一個非法的區域,valgrind 還好心告訴你這個地方是在 mem_test.c:6 allocate 出來的 memory 之後的 3 byte,通常遇到這種情況都是忘記檢查 buffer 的 size 就去用。
錯誤二:Invalid read of size 4,試圖讀取一個非法的區域。
錯誤三:Invalid read of size 4,讀取的區域已經被 free 了,free 的位置 valgrind 也幫你指出來 mem_test.c:12。
錯誤四:Invalid free,也就是 free 一個不存在的地方,或是 double free



其他提醒

遇到 Conditional jump or move depends on uninitialised value(s) 的錯誤
可能是用了沒有結束字元 (Null-terminated string) 的字串

有一種錯誤不太好找,如 A function 用到用到 B function 產生出來的 memory,對於系統本身並無任何違法,但卻會造成程式出現不預期的值。

對於 local variable 的存取如果超過範圍,還有可能造成 stack corrupt,然後噴出這類的錯誤:
*** stack smashing detected ***: ./mem_test terminated
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(__fortify_fail+0x37)[0x64f8f47]
/lib/x86_64-linux-gnu/libc.so.6(__fortify_fail+0x0)[0x64f8f10]
./mem_test(func+0x2db)[0x407f22]
======= Memory map: ========
00400000-00420000 r-xp 00000000 08:01 280804      /tmp/mem_test
0061f000-00620000 r--p 0001f000 08:01 280804      /tmp/mem_test
00620000-00622000 rw-p 00020000 08:01 280804      /tmp/mem_test
00622000-00623000 rw-p 00000000 00:00 0
04000000-04022000 r-xp 00000000 08:01 160861      /lib/x86_64-linux-gnu/ld-2.15.so

如果不想 show possibly lost,可以加下面的參數
3.9版 --show-leak-kinds=definite
3.7版 --show-possibly-lost=no

另外 file descriptor 開了沒關也可以偵測,只要加上 --track-fds=yes

之前有遇過 valgrind 在 ubuntu 12.04 一直有錯誤訊息,記得要安裝額外的套件 sudo apt-get install libc6-dbg
12.04 - Valgrind does debug error - Ask Ubuntu

2014/02/18

AWS VPC 設定教學

在 AWS VPC(Virtual Private Cloud) 介紹中列了四種 scenario,在此以 Scenario 2: VPC with Public and Private Subnets 為例來說明操作過程。
整個過程會建立 public, private 兩個 subnet,public subnet 用來放置對外服務的 instance 具有 public IP,如 web server。而 private subnet,只允許 VPC 內部存取,如 database。


Create VPC

1. 建立 VPC,指定 CIDR block 爲 10.0.0.0/16
Local IP 位址從 10.0.0.0 - 10.0.255.255 共 256*256 個 IP


2. 切兩個 subnet
Public subnet CIDR: 10.0.0.0/16,IP 從 10.0.0.0 - 10.0.0.255
Private subnet CIDR: 10.0.1.0/16,IP 從 10.0.1.0 - 10.0.1.255
預設所有在 VPC 底下任何 subnet 的 IP 都可以互通

2014/02/12

Install YouTube plugin in PhoneGap

I develop app with PhoneGap 3.3.1, and import the YouTube plugin from YTPhoneGapPlugin.

Integrate steps:
1. Use command to install plugin
$ cordova local plugin add https://github.com/matiasmolinas/YTPhoneGapPlugin.git

2. Download YouTube api to project libs folder

3. Register the YouTube API Key from here, and fill the key in
android project/src/gdg/youtube/YouTube.java

4. Implementation
var videoId = "YSxYCMlkLsw";
var success = function() { console.log("Success"); };
var error = function(message) { console.log("Oopsie! " + message); };
window.youtube.playVideo(videoId, success, error);

If you met error message as following:
I/Web Console( 1614): Oopsie! No Activity found to handle Intent { act=com.google.android.youtube.api.StandalonePlayerActivity.START (has extras) } at file:///android_asset/www/app.js:1
Try to install YouTube app in your Android system, if you use emulator, try to download from google play and use command line to install it.
adb install com.google.android.youtube.apk
Reference

2013/08/31

Embedded Sencha Touch Demo Code in My Blog

今天在思考怎麼在我的 blog 嵌入 Sencha Touch 的 library,之後寫 demo code 就可以看到 preview了!


首先選擇必要的 library

Sencha Touch 專案預設是用 micro-loader 載入設定在 app.json 的 js / css 檔,不過檔案太零碎需要發很多 request,若透過 internet 會拖慢速度,SDK 內同時也有提供整合的版本:
  • sencha-touch.js 131KB
  • sencha-touch-debug.js 514KB
  • sencha-touch-all.js 671KB
  • sencha-touch-all-debug.js 2.7M

各版本的用途可以參考 Using and Creating Builds,由於我需要使用到 widget,所以要選 all 的版本,sencha-touch-all.js (671KB) 雖然壓縮過了還是好肥大...

CSS 的部分,SDK 內也提供不同組的 theme 可以套:
  • base.css 230KB
  • bb10.css 184KB BlackBerry
  • sencha-touch.css 258KB
  • wp.css 194KB Windows Phone

2013/08/28

Sencha Touch AJAX 跨網域存取 (Cross Domain Request)

通常為了省麻煩,我們會把『提供網頁的 server』與『提供資料的 API server』放在同一個網域下,除非是存取第三方的 server,但現在大多走 OAuth 配合 token 在做存取...


那為何我會遇到跨網域問題?

是這樣的,使用 PhoneGap 開發 app 時,若要跟 API server 要資料也屬於跨網域存取,不過 PhoneGap 的 webkit 設計不像我們平常在用的 browser,所以不會有此限制。

只是開發過程我們會直接利用 browser debug,不會每次都把網頁放到 PhoneGap 內,然後 build、deploy 到 mobile device... 這實在太花時間了,這就是為何我需要解決 cross domain 的原因。

2013/08/26

PhoneGap 開發:Android back button 事件處理

使用 PhoneGap 開發 app 時,在 Android 平台上,預設按下 back button 的處理方式是關閉程式,這應該會讓使用者崩潰。PhoneGap 有提供對應的 backbutton 事件可以註冊,這樣就可以照自己的方式來處理。


這邊的處理方式是呼叫原生的 confirm 對話框,詢問 user 是否離開:
// Register event for back button
document.addEventListener("backbutton", function() { 
    navigator.notification.confirm(
        '你確定要離開程式嗎?', 
        onConfirmQuit, 
        '關閉', 
        ['確定', '取消']
    );
}, true);

function onConfirmQuit(button) {
    if (button == "1") {
        // Leave app
        navigator.app.exitApp(); 
    }
}


身為一個 Sencha Touch 的使用者,用客制化的視窗也是可以 OK 的
document.addEventListener("backbutton", function() { 
    Ext.Msg.confirm("關閉", "你確定要離開程式嗎?", function(buttonId) {
        if (buttonId == 'yes')
            navigator.app.exitApp();
    });
}, true);

更進階的做法就是讓使用者按下 back 時可以回到上一層的畫面,不過這邊我也還沒試過,目前的想法是啓用 location hash,按下 back 時就像瀏覽器回到上一頁一樣。參考:History Support - Touch 2.2.1 - Sencha Docs