[Laravel5.1踩雷紀錄] 進行整合測試發生1205 Lock wait timeout exceeded; try restarting transaction

前陣子不知道為什麼在我的Laravel app進行整合測試的時候開始出現這個錯誤,而且隨著TestCase開始慢慢增加,問題就越來越嚴重!因為這造成我們在跑測試的時候花費非常多的時間!而且會因為Lock wait timeout導致無法測完某個TestCase就failure

使用情境

版本:

  • PHP 7.0
  • PHPUnit 5.7
  • Laravel 5.1
  • MySQL 5.6

執行phpunit進行測試

vendor/bin/phpunit --stop-on-failure

跑了一部份的TestCase就出現

PDOException: SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction

如果把上面的錯誤訊息直接丟去問Google大神,那找到的解法可能無法解決這個情境發生的問題,因為有太多的原因會造成Lock wait timeout還好這個情況是在測試時發生的,需要確認的因素單純很多,其實追根究底Lock wait timeout exceeded就是transaction一直在等待等到timeout,如果我們可以觀察目前transaction的狀態那一定可以幫助我們釐清這個問題

在MySQL內建資料庫information_schema的INNODB_TRX 資料表可以讓我們觀察目前正在執行或等待的transaction

The INNODB_TRX table contains information about every transaction (excluding read-only transactions) currently executing inside InnoDB, including whether the transaction is waiting for a lock, when the transaction started, and the SQL statement the transaction is executing, if any.

MySQL Documentation

從這張gif看就可以很明顯的發現,隨著測試開始進行時transaction的數量竟然慢慢飆高,而且一直都在running的狀態

因為我是用Laravel提供的DatabaseTransactions Trait,照理來說跑完某個Test就要rollback才對,但竟然沒有!實際去看DatabaseTransactions.php也確實有rollback,有趣的是它在beforeApplicationDestroyed的時候才執行

    public function beginDatabaseTransaction()
    {
        $this->app->make('db')->beginTransaction();

        $this->beforeApplicationDestroyed(function () {
            $this->app->make('db')->rollBack();
        });
    }

在從beforeApplicationDestroyed()找到vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php這隻檔案

abstract class TestCase extends PHPUnit_Framework_TestCase
{
    use ApplicationTrait, AssertionsTrait, CrawlerTrait;

    /**
     * The callbacks that should be run before the application is destroyed.
     *
     * @var array
     */
    protected $beforeApplicationDestroyedCallbacks = [];

    /**
     * Creates the application.
     *
     * Needs to be implemented by subclasses.
     *
     * @return \Symfony\Component\HttpKernel\HttpKernelInterface
     */
    abstract public function createApplication();

    /**
     * Setup the test environment.
     *
     * @return void
     */
    public function setUp()
    {
        if (! $this->app) {
            $this->refreshApplication();
        }
    }

    /**
     * Clean up the testing environment before the next test.
     *
     * @return void
     */
    public function tearDown()
    {
        if ($this->app) {
            foreach ($this->beforeApplicationDestroyedCallbacks as $callback) {
                call_user_func($callback);
            }

            $this->app->flush();

            $this->app = null;
        }

        if (property_exists($this, 'serverVariables')) {
            $this->serverVariables = [];
        }

        if (class_exists('Mockery')) {
            Mockery::close();
        }
    }

    /**
     * Register a callback to be run before the application is destroyed.
     *
     * @param  callable  $callback
     * @return void
     */
    protected function beforeApplicationDestroyed(callable $callback)
    {
        $this->beforeApplicationDestroyedCallbacks[] = $callback;
    }
}

看完以上的程式碼應該就可以瞭解DatabaseTransactions Trait的運作流程

  1. 在呼叫beginDatabaseTransaction()的時候, 開啟transaction並將rollback callback存入beforeApplicationDestroyedCallbacks陣列
  2. 在呼叫tearDown()的時候才會取出rollback callback執行

到目前為止幾乎可以確定是transaction開啟後但沒有rollback,重新去檢查之後才發現某些TestCase覆寫了tearDown method卻沒有呼叫parent::tearDown()

public function tearDown()
{
    //some code here.
}

改成

public function tearDown()
{
    parent::tearDown();
    //some code here.
}

就可以讓transaction rollback

總結前因後果

tearDown()被覆寫了,但在child method沒有呼叫parent::tearDown()導致Test跑完了transaction卻一直沒有被釋放掉,進而發生transaction wait timeout的問題,如果TestCase數量很少的時候這個問題其實不會太明顯,但隨著Test的數量越來越多這個問題就很難被忽視

補充

其實還有另一種方法可以解決,但還是建議使用第一個解法!畢竟有些時候會需要用同一個PHP process進行測試,PHPUnit提供了–process-isolation參數,這個參數讓PHPUnit在跑每個測試的時候都是用新的php process執行

vendor/bin/phpunit --stop-on-failure --process-isolation

[Nginx]接收自定義header需要注意的事情

我們都知道HTTP的header可以塞自定義的內容,身為一個Web Develop Developer開發的API在大部分的情況下都會要求client side提供一個token或key來讓server side驗證這個request是否允許存取這個API,當然這個token必須是由server side產生的,這時候我們就需要請client side把token塞在request header裡面,server side會去驗證這個token是不是合法的

Nginx會將含有底線的header視為不合法

Nginx預設的設定會將含有底線(underscore)的欄位名稱視為不合法的header,導致server side的程式碼無法取得自定義的header內容

解決這個問題有兩個方法

1.不要在header field name使用底線

2.在nginx.conf啟用underscores_in_headers:

underscores_in_headers on;

為了查明這個問題還特地去看了一下nginx的source code

        rc = ngx_http_parse_header_line(r, r->header_in,
                                        cscf->underscores_in_headers);

        if (rc == NGX_OK) {

            r->request_length += r->header_in->pos - r->header_name_start;

            if (r->invalid_header && cscf->ignore_invalid_headers) {

                /* there was error while a header line parsing */

                ngx_log_error(NGX_LOG_INFO, c->log, 0,
                              "client sent invalid header line: \"%*s\"",
                              r->header_end - r->header_name_start,
                              r->header_name_start);
                continue;
            }

            /* a header line has been parsed successfully */

            h = ngx_list_push(&r->headers_in.headers);
            if (h == NULL) {
                ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
                return;
            }

            h->hash = r->header_hash;

            h->key.len = r->header_name_end - r->header_name_start;
            h->key.data = r->header_name_start;
            h->key.data[h->key.len] = '\0';

            h->value.len = r->header_end - r->header_start;
            h->value.data = r->header_start;
            h->value.data[h->value.len] = '\0';

            h->lowcase_key = ngx_pnalloc(r->pool, h->key.len);
            if (h->lowcase_key == NULL) {
                ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
                return;
            }

            if (h->key.len == r->lowcase_index) {
                ngx_memcpy(h->lowcase_key, r->lowcase_header, h->key.len);

            } else {
                ngx_strlow(h->lowcase_key, h->key.data, h->key.len);
            }

            hh = ngx_hash_find(&cmcf->headers_in_hash, h->hash,
                               h->lowcase_key, h->key.len);

            if (hh && hh->handler(r, h, hh->offset) != NGX_OK) {
                return;
            }

            ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                           "http header: \"%V: %V\"",
                           &h->key, &h->value);

            continue;
        }

從上面的程式碼可以看到,如果被nginx視為不合法的(invalid)header就會將client sent invalid header line xxx寫入log,並且不會繼續往下執行而是直接continue

所以如果在server side的程式碼無法取得自定義的header內容且滿足以下條件:

  1. server side使用的web server是nginx
  2. 確定client side有給server side自定義的header而且這個header含有底線的名稱
  3. 在server side的程式碼無法取得自定義的header內容
  4. underscores_in_header沒開

就打開underscores_in_headerreload nginx

含有底線的header被視為不合法的原因

這是為了避免將header fileds name傳遞到CGI變數的時候造成衝突,因為nginx將header映設給CGI變數的時候會將破折號(dashes)和底線(underscores)都轉成底線,如果自定義的header有兩個MyTokenMy_Token

GET /
Host: tonyhao.net
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Cookie: PHPSESSID=r2t5uvjq435r4q7ib3vtdjq120
Pragma: no-cache
Cache-Control: no-cach
My-Token: my-token1
My_Token: my-token2

映設到CGI變數的時候都會變成My_Token,在這個情況下就會發生衝突了

參考資料

https://github.com/nginx/nginx/blob/master/src/http/ngx_http_request.c

http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_header

https://code.tutsplus.com/tutorials/http-headers-for-dummies–net-8039

[SQL]having的使用時機

一般來說having常常搭配group by一起使用

假設有一張articles的資料表,共有10筆資料如下:

現在有個情境是「取得文章分類的流覽次數超過5且依照次數由高到低排序」

若是一個初學SQL的新手又不認識having的用法,可能會寫成這樣:

SELECT
	category_id,
	SUM(view_cnt) AS view_cnt
FROM
	`articles`
WHERE
	view_cnt > 5;
GROUP BY
	category_id
ORDER BY
	view_cnt DESC

乍看之下好像是對的,但其實這個SQL變成「取得文章流覽次數超過5的文章分類流覽次數並依照次數由高到低排序」,這是因為WHERE會套用在GROUP BY之前,所以在GROUP BY前會把文章瀏覽次數小於5的排除掉才進行GROUP BY,這個結果不是我們要的!

正確的SQL是利用GROUP BY搭配HAVING

SELECT
	category_id, SUM(view_cnt)  AS total_view_cnt
FROM
	`articles`
GROUP BY category_id
HAVING total_view_cnt > 5
ORDER BY total_view_cnt DESC

如果不想使用HAVING的話也可以利用子查詢的方式,但比較不建議

SELECT * FROM (
SELECT
	category_id, SUM(view_cnt)  AS total_view_cnt
FROM
	`articles`
GROUP BY category_id
ORDER BY total_view_cnt DESC
) A
WHERE A.total_view_cnt > 5;

因為查兩次,多此一舉

[Linux]利用nslookup指定DNS查詢Domain

總該有個前因

今天遇到一個突發狀況,DNS(Domain Name Server)上的子網域設定突然失效導致客戶無法使用子網域下的服務

在查明是DNS的問題之前,腦海中有閃過一個指令是可以用來指定DNS查詢Domain

後來詢問Google大神才喚醒我的記憶

在解決這個突發狀況的時候nslookup幫了我很大的忙

正好趁這個機會重溫一下nslookup這個指令

我記得第一次接觸它是在大學修網路概論課的時候

一句話說明nslookup的作用

nslookup是用來查詢網路上的DNS

我們都知道網路上有非常多DNS,常見的有:

中華電信

  • 168.95.1.1
  • 168.95.192.1

Google

  • 8.8.8.8
  • 8.8.4.4

我們在本機可以設定要去哪一台DNS查詢Domain

那如果在不更動本機的DNS設定下,可以使用別台的DNS來查詢Domain嗎?

答案是Yes, 對!就是利用nslookup

如何使用nslookup

nslookup提供interactive和non-interactive兩種模式來查詢

non-interactive

nslookup google.com #使用本機設定的DNS查詢google.com這個domain
nslookup google.com 8.8.8.8 #到8.8.8.8這台DNS查詢google.com這個domain

interactive

nslookup
> server 8.8.8.8 #指定DNS
> google.com 8.8.8.8 #查詢google.com這個domain

以上兩種方式都有人在用,看個人習慣

自己比較偏好第二個

[vue.js] v-model.lazy的使用時機

vue.js的v-model directive是一個非常方便的東西

由於v-model是使用two way data binding

所以只要修改就會立即顯示

但有時候我們也會希望v-model不要這麼快就幫我們sync

尤其是使用者在填寫表單(form)的時候

我們會希望使用者填完之後才觸發

這時候就可以使用v-model.lazy

加上.lazy這個內建的modifier讓data sync在change event事件後才去處理

vuejs v2:

<input v-model.lazy="name">

參考來源:

https://vuejs.org/v2/guide/forms.html#lazy

javascript ES6 的箭頭函式(arrow function)

javascript ES6的arrow function是方便的syntax sugar,可以幫助我們寫出更簡潔的js code

在arrow function還沒出現前,針對特定element綁定事件大致會像這樣:

document
.getElementById('my-btn')
.addEventListener("click", function(e) => {
alert('before es6');
});

將以下的anonymous function替換成arrow function的寫法:

document
.getElementById('my-btn')
.addEventListener("click", (e) => {
alert('arrow function');
});

只有一個參數時,可省略括號:

document
.getElementById('my-btn')
.addEventListener("click", e => {
alert('arrow function');
});

若只有一行statement時,可以省略大括號

document
.getElementById('my-btn')
.addEventListener("click", e => alert('arrow function'));

其實從以上的範例還真看不出來arrow function強大的地方XD
頂多就只有省略function這個關鍵字和大括號而已
但其實這只是arrow function的其中一種寫法
更方便的用法還在後頭~

arrow function讓你不用.bind(this)

我們都知道javascript的this是一個很tricky的東西
和Java、C#、PHP等OOP的語言的this所代表的涵意是不太一樣的

言規正傳,如果不使用arrow function在setTimeout的callback中要取得object instance的this
就要使用.bind把object instance的this綁定給setTimeout的callback

function UserModel(id) {
    this.id = id;
    this.name = '';

    setTimeout(function() {
	this.name = 'from remote';
    }.bind(this), 1000);
}

var userModel = new UserModel(1);
console.log(userModel.name);

但如果使用arrow function就簡單的多了,不需要加上.bind(this)

function UserModel(id) {
    this.id = id;
    this.name = '';

    setTimeout(() => {
	this.name = 'from remote';
    }, 1000);
}

var userModel = new UserModel(1);
console.log(userModel.name);

結論

ES6的arrow function可以讓我們少寫一些程式碼
更容易處理this的指向,進而寫出更簡潔易懂的程式碼

[PHP] 為什麼應該使用MySQL Native Driver(mysqlnd)來取代MySQL Client Library(libmysqlclient)

還記得以前第一次自己架設LAMP開發環境的時候,就已經注意到安裝有這兩個package了

但那時候還不曉得有何差別,只記得書上叫我裝php5-mysql就照著裝XD

前陣子碰到一個需要安裝mysqlnd才能解決的問題,正好趁這個機會來瞭解一下差別

MySQL Native Driver is a replacement for the MySQL Client Library (libmysqlclient)

在PHP文件的myslqnd章節內一開頭就說明了mysqlnd是用來取代libmysqlclient

為何PHP官方會建議使用mysqlnd?

 

mysqlnd是屬於PHP Project的一部份

不像libmysqlclient是使用MySQL license,mysqlnd是使用PHP license這也避免了一些因為license所衍生的問題(看看前陣子Oracle告Google的新聞就知道為什麼了!)

mysqlnd的功能比libmysqlclient還多

從PHP 5.3.0開始mysqlnd library 已經是內建在PHP的library,mysqlnd提供的一些功能像是query caching、lazy connections、SSL這些在libmysqlclient的沒辦法用的

詳細差異請參閱Library feature comparison

mysqlnd的效能比libmysqlclient還好

由於mysqlnd是用C寫的PHP extension,所以它是使用PHP memory management system也支援PHP memory limit和memory_get_usage(),安裝mysqlnd可以使用memory_get_usage()來追蹤記憶體的使用情形,而是在libmysqlclient是不可能的,因為libmysqlclient要使用C語言的function malloc(),在官網有舉一個例子

One example of the memory efficiency is the fact that when using the MySQL Client Library, each row is stored in memory twice, whereas with the MySQL Native Driver each row is only stored once in memory.

libmysqlclient在儲存每一筆資料row到memory的時候會存兩次

而mysqlnd只會存一次

 

參考來源:

http://php.net/manual/en/mysqlnd.overview.php

http://php.net/manual/en/mysqlinfo.library.choosing.php

http://php.net/manual/en/intro.mysqlnd.php

https://dev.mysql.com/downloads/connector/php-mysqlnd/