OpenResty Lua鉤子調用完整流程

前面一篇文章介紹了Openresty Lua協程調度機制,主要關心的是核心調度函數run_thread()內部發生的事情,而對於外部的事情我們並沒有涉及。本篇作為其姊妹篇,準備補上剩餘的部分。本篇將通過一個例子,完整介紹OpenResty中Lua鉤子的調用流程,包括初始化階段的工作、新連接進來時如何進入鉤子、I/O等待時如何出去、事件觸發時如何恢復、鉤子正常執行結束時的操作、鉤子內出錯的情況。本文同樣是基於stream-lua模組的程式碼。

本部落格已經遷移至CatBro’s Blog,那裡是我自己搭建的個人部落格,頁面效果比這邊更好,支援站內搜索,評論回復還支援郵件提醒,歡迎關注。這邊只會在有時間的時候不定期搬運一下。

本篇文章鏈接

整體流程

我們以ssl_certificate_by_lua*鉤子為例來進行介紹,一來是因為它還涉及SSL握手,流程上更長一點。二來是因為在其上下文中是YIELDABLE的,支援的ngx介面比較完整。

我們將以下面兩個配置為例來展開介紹。例子非常簡單,第一個是正常的結束情況在ssl_certificate_by_lua_block裡面調用了ngx.sleep()。第二個是出錯中止的情況,多了一個ngx.exit(ngx.ERROR)

server {
    listen 443 ssl;
    ssl_certificate_by_lua_block {
        ngx.sleep(0.1)
    }
    ssl_certificate test.pem;
    ssl_certificate_key test.key;
}
server {
    listen 443 ssl;
    ssl_certificate_by_lua_block {
        ngx.sleep(0.1)
        ngx.exit(ngx.ERROR)
    }
    ssl_certificate test.pem;
    ssl_certificate_key test.key;
}

首先,分別來看一下初始化階段和連接階段的整體流程。後面章節會結合實際程式碼,來詳細介紹每種情況下是如何處理的。

初始化階段

openresty-lua-hook-flow-init-phase

初始化階段的流程比較簡單:配置解析階段會讀取配置文件中的程式碼塊進行解析保存,然後創建Lua程式碼的key,這個key是用於後面將程式碼cache到註冊表的。配置合併階段,主要是合併配置項,然後設置cert_cb回調。配置後處理階段,主要工作是初始化Lua VM,包括創建註冊表項、創建全局表項ngx、替換coroutine介面。

連接階段

openresty-lua-hook-flow-connect-phase

連接階段因為涉及新連接進入鉤子、I/O等待時出去、事件觸發時恢復、鉤子正常執行結束(YIELD之後)、鉤子內出錯(YIELD之後)等各種情況,相對比較複雜。圖中用不同顏色分別表示這幾種不同的情況,每種顏色又用數字標識了其流程順序。讀者可以結合這個圖,閱讀後續每個階段的程式碼,應該能夠幫助您更好地理解。

另外提一下,本文沒有涉及Lua程式碼執行過程沒有碰到YIELD就直接完成或者出錯的情況。因為這種情況比較簡單,整個流程都是一個同步的過程。執行完成或者出錯之後,lua_resume()返回,後續的流程就跟圖中I/O等待(棕色)的情況是一樣的。

初始化階段

配置項解析

解析到ssl_certificate_by_lua_block時會調用ngx_stream_lua_ssl_cert_by_lua_block()進行解析,裡面會進行配置文件的詞法分析,將程式碼塊中的程式碼都合併到一個buffer之後,插入到參數數組的後面。然後調用ngx_stream_lua_ssl_cert_by_lua()。(如果是by_lua_file的情況會直接調用ngx_stream_lua_ssl_cert_by_lua()

char *
ngx_stream_lua_ssl_cert_by_lua_block(ngx_conf_t *cf, ngx_command_t *cmd,
    void *conf)
{
    char        *rv;
    ngx_conf_t   save;

    save = *cf;
    cf->handler = ngx_stream_lua_ssl_cert_by_lua;
    cf->handler_conf = conf;

    rv = ngx_stream_lua_conf_lua_block_parse(cf, cmd);

    *cf = save;

    return rv;
}

ngx_stream_lua_ssl_cert_by_lua()主要工作是設置lscf->srv.ssl_cert_src以及創建Lua程式碼的key。如果是by_lua_file的情況,key以字元串nhlf_開頭,後邊是對文件路徑計算的摘要十六進位值;而by_lua_block的情況,key以字元串"ssl_certificate_by_lua"開頭,後邊是對整個Lua程式碼塊計算的摘要十六進位值。

lscf->srv.ssl_cert_src = value[1];

p = ngx_palloc(cf->pool,
               sizeof("ssl_certificate_by_lua") +
               NGX_STREAM_LUA_INLINE_KEY_LEN);
if (p == NULL) {
    return NGX_CONF_ERROR;
}

lscf->srv.ssl_cert_src_key = p;

p = ngx_copy(p, "ssl_certificate_by_lua",
             sizeof("ssl_certificate_by_lua") - 1);
p = ngx_copy(p, NGX_STREAM_LUA_INLINE_TAG, NGX_STREAM_LUA_INLINE_TAG_LEN);
p = ngx_stream_lua_digest_hex(p, value[1].data, value[1].len);
*p = '\0';

配置項合併

在配置合併階段,由ngx_stream_lua_merge_srv_conf()cert_cb回調函數ngx_stream_lua_ssl_cert_handler()設置到server的SSL_CTX上。

        /* 先進行配置合併 */
        if (conf->srv.ssl_cert_src.len == 0) {
            conf->srv.ssl_cert_src = prev->srv.ssl_cert_src;
            conf->srv.ssl_cert_src_key = prev->srv.ssl_cert_src_key;
            conf->srv.ssl_cert_handler = prev->srv.ssl_cert_handler;
        }
        /* 如果設置了該配置 */
        if (conf->srv.ssl_cert_src.len) {
            if (sscf->ssl.ctx == NULL) {
                ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
                              "no ssl configured for the server");

                return NGX_CONF_ERROR;
            }

#   if OPENSSL_VERSION_NUMBER >= 0x1000205fL
            /* 設置cert_cb回調 */
            SSL_CTX_set_cert_cb(sscf->ssl.ctx, ngx_stream_lua_ssl_cert_handler, NULL);

#   else
            /* ... */
#   endif
        }

配置後處理 postconfiguration

在postconfig階段,會調用ngx_stream_lua_init(),它裡面最關鍵的任務就是初始化Lua VM。(其實還會調用init_by*鉤子,不過不在我們今天的討論範圍內。)

rc = ngx_stream_lua_init_vm(&lmcf->lua, NULL, cf->cycle, cf->pool,
                            lmcf, cf->log, NULL);

我們來看下ngx_stream_lua_init_vm()裡面的實現,它先會創建Lua VM實例,然後註冊其cleanup handler,如果有第三方模組的preload_hooks會註冊之,然後會載入resty.core模組,最後會注入程式碼對全局變數的寫操作加一個警告日誌。

    /* create new Lua VM instance */
    L = ngx_stream_lua_new_state(parent_vm, cycle, lmcf, log);
    if (L == NULL) {
        return NGX_ERROR;
    }

    /* register cleanup handler for Lua VM */
    cln->handler = ngx_stream_lua_cleanup_vm;

    state = ngx_alloc(sizeof(ngx_stream_lua_vm_state_t), log);
    if (state == NULL) {
        return NGX_ERROR;
    }
    state->vm = L;
    state->count = 1;

    cln->data = state;

    if (lmcf->vm_cleanup == NULL) {
        /* this assignment will happen only once,
         * and also only for the main Lua VM */
        lmcf->vm_cleanup = cln;
    }

#ifdef OPENRESTY_LUAJIT
    /* load FFI library first since cdata needs it */
    luaopen_ffi(L);
#endif

    if (lmcf->preload_hooks) {
        /* 註冊第三方preload_hooks */
    }

    *new_vm = L;

    lua_getglobal(L, "require");
    lua_pushstring(L, "resty.core");

    rc = lua_pcall(L, 1, 1, 0);
    if (rc != 0) {
        return NGX_DECLINED;
    }

#ifdef OPENRESTY_LUAJIT
    ngx_stream_lua_inject_global_write_guard(L, log);
#endif

    return NGX_OK;

關鍵函數是創建Lua VM實例的ngx_stream_lua_new_state(),我們來一睹其芳容:

/* 創建vm state*/
L = luaL_newstate();
/* 打開標準庫 */
luaL_openlibs(L);
/* 獲取package表 */
lua_getglobal(L, "package");

/* 設置package.path和package.cpath */

lua_pop(L, 1); /* remove the "package" table */

/* 初始化registry */
ngx_stream_lua_init_registry(L, log);
/* 初始化globals */
ngx_stream_lua_init_globals(L, cycle, lmcf, log);

return L;

重點是最後的兩個函數,它們分別初始化registryglobals。這個兩個函數都不算太長,讓我們來完整看下它們做了些什麼。

ngx_stream_lua_init_registry()創建了幾個註冊表項,分別用於存放協程、Lua的請求ctx、socket連接池、Lua預編譯正則表達式對象cache及Lua程式碼cache。

ngx_stream_lua_init_registry(lua_State *L, ngx_log_t *log)
{
    ngx_log_debug0(NGX_LOG_DEBUG_STREAM, log, 0,
                   "lua initializing lua registry");

    /* {{{ register a table to anchor lua coroutines reliably:
     * {([int]ref) = [cort]} */
    lua_pushlightuserdata(L, ngx_stream_lua_lightudata_mask(
                          coroutines_key));
    lua_createtable(L, 0, 32 /* nrec */);
    lua_rawset(L, LUA_REGISTRYINDEX);
    /* }}} */

    /* create the registry entry for the Lua request ctx data table */
    lua_pushliteral(L, ngx_stream_lua_ctx_tables_key);
    lua_createtable(L, 0, 32 /* nrec */);
    lua_rawset(L, LUA_REGISTRYINDEX);

    /* create the registry entry for the Lua socket connection pool table */
    lua_pushlightuserdata(L, ngx_stream_lua_lightudata_mask(
                          socket_pool_key));
    lua_createtable(L, 0, 8 /* nrec */);
    lua_rawset(L, LUA_REGISTRYINDEX);

#if (NGX_PCRE)
    /* create the registry entry for the Lua precompiled regex object cache */
    lua_pushlightuserdata(L, ngx_stream_lua_lightudata_mask(
                          regex_cache_key));
    lua_createtable(L, 0, 16 /* nrec */);
    lua_rawset(L, LUA_REGISTRYINDEX);
#endif

    /* {{{ register table to cache user code:
     * { [(string)cache_key] = <code closure> } */
    lua_pushlightuserdata(L, ngx_stream_lua_lightudata_mask(
                          code_cache_key));
    lua_createtable(L, 0, 8 /* nrec */);
    lua_rawset(L, LUA_REGISTRYINDEX);
    /* }}} */
}

ngx_stream_lua_init_globals()則是創建了ngx表,接著把相關Lua Ngx API全部註冊到全局表上了,其中就包括我們前面例子中的ngx.sleep()ngx.exit()。然後把ngx表分別設為全局表項,同時也設到package.loaded.ngx了。注意,原生的coroutine介面也在這裡被替換了。

static void
ngx_stream_lua_inject_ngx_api(lua_State *L, ngx_stream_lua_main_conf_t *lmcf,
    ngx_log_t *log)
{
    lua_createtable(L, 0 /* narr */, 113 /* nrec */);    /* ngx.* */

    lua_pushcfunction(L, ngx_stream_lua_get_raw_phase_context);
    lua_setfield(L, -2, "_phase_ctx");


    ngx_stream_lua_inject_core_consts(L);

    ngx_stream_lua_inject_log_api(L);
    ngx_stream_lua_inject_output_api(L);
    ngx_stream_lua_inject_string_api(L);
    ngx_stream_lua_inject_control_api(log, L);


    ngx_stream_lua_inject_sleep_api(L);
    ngx_stream_lua_inject_phase_api(L);

    ngx_stream_lua_inject_req_api(log, L);


    ngx_stream_lua_inject_shdict_api(lmcf, L);
    ngx_stream_lua_inject_socket_tcp_api(log, L);
    ngx_stream_lua_inject_socket_udp_api(log, L);
    ngx_stream_lua_inject_uthread_api(log, L);
    ngx_stream_lua_inject_timer_api(L);
    ngx_stream_lua_inject_config_api(L);

    lua_getglobal(L, "package"); /* ngx package */
    lua_getfield(L, -1, "loaded"); /* ngx package loaded */
    lua_pushvalue(L, -3); /* ngx package loaded ngx */
    lua_setfield(L, -2, "ngx"); /* ngx package loaded */
    lua_pop(L, 2);

    lua_setglobal(L, "ngx");

    ngx_stream_lua_inject_coroutine_api(log, L);
}

小結

初始化階段的主要工作就是這些,簡單小結一下,配置項解析階段完成了Lua程式碼key的創建,配置項合併階段完成了Lua鉤子回調的設置,postconfig階段完成了Lua虛擬機的初始化,其中包括registry和globals的初始化。當master進程fork出worker子進程之後,每個worker都將有一個自己的Lua VM實例。

進入Lua鉤子

接下來,我們來看連接發起階段。當監聽的socket接收到連接請求之後,會調用accept建立連接,因為是stream子系統調用到ngx_stream_init_connection,又因為是ssl server會先走到ngx_stream_ssl_handler,裡面調用ngx_ssl_create_connection創建連接(SSL_new(ssl->ctx)),最終會調用SSL_do_handshake進入SSL狀態機。

for ( ;; ) {
    ngx_process_events_and_timers(cycle)
    +-- ngx_epoll_process_events()
        |-- epoll_wait()
        +-- ngx_event_accept()
            |-- accept4()
            |-- ngx_get_connection()
            +-- ngx_stream_init_connection()
                +-- ngx_stream_session_handler()
                    |-- s = ngx_pcalloc(c->pool, sizeof(ngx_stream_session_t))
                    +-- ngx_stream_core_run_phases()
                        +-- ngx_stream_core_generic_phase()
                            +-- ngx_stream_ssl_handler()
                                +-- ngx_stream_ssl_init_connection()
                                    |-- ngx_ssl_create_connection()
                                    |   +-- SSL_new(ssl->ctx)
                                    +-- ngx_ssl_handshake()
                                        |-- SSL_do_handshake()
                                        |-- sslerr = SSL_get_error();
                       
}

SSL狀態機的部分不是我們今天的重點,這裡暫且略過。

ossl_statem_accept()
+-- state_machine()
    +-- read_state_machine()
        +-- ossl_statem_server_post_process_message()

狀態機最終會調用到tls_post_process_client_hello()里的cert_cb。這個回調我們已經在配置初始化階段設置了,在創建SSL連接的時候又會拷貝到SSL結構體里。

tls_post_process_client_hello()
+-- s->cert->cert_cb(); /* 即ngx_stream_lua_ssl_cert_handler */
    |   /* 即ngx_stream_lua_ssl_cert_handler_inline */
    +-- lscf->srv.ssl_cert_handler(r, lscf, L); 
        |-- ngx_stream_lua_cache_loadbuffer()
        +-- ngx_stream_lua_ssl_cert_by_chunk()
            |-- ngx_stream_lua_create_ctx()
            |-- lua_xmove(L, co, 1);    /* 將程式碼閉包從L移到co上 */
            |-- ngx_stream_lua_new_thread()
            +-- ngx_stream_lua_run_thread() 
                |-- lua_resume()

ngx_stream_lua_ssl_cert_handler中會做一些初始化工作,如創建fake連接、fake會話、fake請求(因為還在SSL握手階段,還沒有真實的前端請求),設置默認的返回碼。

fc = ngx_stream_lua_create_fake_connection(NULL);
fs = ngx_stream_lua_create_fake_session(fc);
r = ngx_stream_lua_create_fake_request(fs);
cctx->exit_code = 1;  /* successful by default */
cctx->connection = c;
cctx->request = r;
cctx->entered_cert_handler = 1;
cctx->done = 0;
SSL_set_ex_data(c->ssl->connection, ngx_stream_lua_ssl_ctx_index, 
                cctx)

然後因為是用配置指令是xxx_by_lua_block所以調用ngx_stream_lua_ssl_cert_handler_inline,它裡面會載入Lua程式碼。如果是第一次載入會把程式碼塊載入為一個Lua函數閉包工廠,然後保存閉包工廠到虛擬機的註冊表上並生成一個閉包到棧頂;後續會直接從虛擬機註冊表上查找並生成閉包到棧頂。

ngx_int_t
ngx_stream_lua_ssl_cert_handler_inline(ngx_stream_lua_request_t *r,
    ngx_stream_lua_srv_conf_t *lscf, lua_State *L)
{
        rc = ngx_stream_lua_cache_loadbuffer(r->connection->log, L,
                                         lscf->srv.ssl_cert_src.data,
                                         lscf->srv.ssl_cert_src.len,
                                         lscf->srv.ssl_cert_src_key,
                                         "=ssl_certificate_by_lua");
        return ngx_stream_lua_ssl_cert_by_chunk(L, r);
}

接下來就是進入by_chunk()準備執行Lua程式碼了,這裡首先創建模組ctx,接著在虛擬機上創建一個入口執行緒,並把程式碼閉包從虛擬機棧上移到新執行緒的棧上,還在fake請求上掛了一個cleanup。然後就是調用run_thread()進入協程調度循環了。裡面的事情我們已經在上一篇中講到了,lua_resume()開始執行我們的Lua程式碼。

ctx = ngx_stream_lua_create_ctx(r->session);
ctx->entered_content_phase = 1;
/* 創建入口執行緒 */
co = ngx_stream_lua_new_thread(r, L, &co_ref);
/* 將程式碼閉包移到入口執行緒中 */
lua_xmove(L, co, 1);
/* 設置閉包的環境表為新協程的全局表 */
ngx_stream_lua_get_globals_table(co);
lua_setfenv(co, -2);
/* 把nginx請求保存到協程全局表中 */
ngx_stream_lua_set_req(co, r);

/* 註冊請求的cleanup hooks */
if (ctx->cleanup == NULL) {
    cln = ngx_stream_lua_cleanup_add(r, 0);
    if (cln == NULL) {
        rc = NGX_ERROR;
        ngx_stream_lua_finalize_request(r, rc);
        return rc;
    }

    cln->handler = ngx_stream_lua_request_cleanup_handler;
    cln->data = ctx;
    ctx->cleanup = &cln->handler;
}

ctx->context = NGX_STREAM_LUA_CONTEXT_SSL_CERT;
rc = ngx_stream_lua_run_thread(L, r, ctx, 0);

I/O等待掛起

我們在初始化階段已經將Lua Ngx API設置到全局表中了,所以ngx.sleep()會調用到對應的C函數ngx_stream_lua_ngx_sleep(),裡面主要是設置了一個定時器,其事件的handler是ngx_stream_lua_sleep_handler()。掛完定時器,就直接lua_yield()了。

    coctx->sleep.handler = ngx_stream_lua_sleep_handler;
    coctx->sleep.data = coctx;
    coctx->sleep.log = r->connection->log;

    ngx_add_timer(&coctx->sleep, (ngx_msec_t) delay);
    return lua_yield(L, 0);

回到我們的主執行緒run_thread()之後,因為是I/O等待就直接返回NGX_AGAIN

rv = lua_resume(orig_coctx->co, nrets);
switch (rv) {
    case LUA_YIELD:
        switch (ctx->co_op) {
            case NGX_STREAM_LUA_USER_CORO_NOP:
                ctx->cur_co_ctx = NULL;
                return NGX_AGAIN;
        }
}

這樣又回到了我們的by_chunk()函數,因為返回值是NGX_AGAIN所以會檢查先隊列裡面有沒有posted的協程,如果有的話會去恢復協程的執行,在我們這個例子是沒有的,不過它的返回值rc改成了NGX_DONE,所以ngx_stream_lua_finalize_request(r, rc);里啥也沒幹就返回了。

    rc = ngx_stream_lua_run_thread(L, r, ctx, 0);

    if (rc == NGX_ERROR || rc >= NGX_OK) {
        /* do nothing */

    } else if (rc == NGX_AGAIN) {
        rc = ngx_stream_lua_content_run_posted_threads(L, r, ctx, 0);

    } else if (rc == NGX_DONE) {
        rc = ngx_stream_lua_content_run_posted_threads(L, r, ctx, 1);

    } else {
        rc = NGX_OK;
    }

    ngx_stream_lua_finalize_request(r, rc);
    return rc;

這個NGX_DONE的返回值往回傳遞到ngx_stream_lua_ssl_cert_handler,在這裡會對不同返回值做不同處理。如果是完成NGX_OK或出錯NGX_ERROR的情況,就意味著鉤子的工作已經結束了。我們目前的返回值是NGX_DONE,說明工作還沒有結束,它在返回-1之前,掛了兩個cleanup。其中_done()的那個是掛在fake連接的pool上的,而_aborted()那個是是掛在前端連接上的。所以_done()函數上在鉤子工作結束之後調用的,而_aborted()是在前端連接終止的時候調用。

 rc = lscf->srv.ssl_cert_handler(r, lscf, L);
/* 已經處理完畢或者出錯的情況 */
if (rc >= NGX_OK || rc == NGX_ERROR) {
    cctx->done = 1;
    ...;
    return cctx->exit_code;
}
/* rc == NGX_DONE */

cln = ngx_pool_cleanup_add(fc->pool, 0);

cln->handler = ngx_stream_lua_ssl_cert_done;
cln->data = cctx;

if (cctx->cleanup == NULL) {
    cln = ngx_pool_cleanup_add(c->pool, 0);

    cln->data = cctx;
    cctx->cleanup = &cln->handler;
}

*cctx->cleanup = ngx_stream_lua_ssl_cert_aborted;

return -1;

這樣就回到了OpenSSL的領地,我們看看出去的流程是怎麼樣的。因為上層的返回值是-1,這裡設置狀態為SSL_X509_LOOKUP然後返回WORK_MORE_B

int rv = s->cert->cert_cb(s, s->cert->cert_cb_arg);
if (rv < 0) {
    s->rwstate = SSL_X509_LOOKUP;
    return WORK_MORE_B;
}

這個返回值傳遞到read_state_machine,變成了返回SUB_STATE_ERROR

case READ_STATE_POST_PROCESS:
    st->read_state_work = post_process_message(s, st->read_state_work);
    switch (st->read_state_work) {
    case WORK_ERROR:
        check_fatal(s, SSL_F_READ_STATE_MACHINE);
        /* Fall through */
    case WORK_MORE_A:
    case WORK_MORE_B:
    case WORK_MORE_C:
        return SUB_STATE_ERROR;

傳遞到state_machine,變成了返回-1。最終ossl_statem_acceptSSL_do_handshake()都返回這個值。

        if (st->state == MSG_FLOW_READING) {
            ssret = read_state_machine(s);
            if (ssret == SUB_STATE_FINISHED) {
                st->state = MSG_FLOW_WRITING;
                init_write_state_machine(s);
            } else {
                /* NBIO or error */
                goto end;
            }

看看回到nginx之後做了什麼,因為返回值是-1,所以會先去獲取錯誤類型,因為之前在cert_cb()返回以後已經設置了s->rwstate = SSL_X509_LOOKUP;所以會返回SSL_ERROR_WANT_X509_LOOKUP,這裡將讀寫事件的回調設置為ssl握手的回調以便下次恢復。

n = SSL_do_handshake(c->ssl->connection);
/* ... */
sslerr = SSL_get_error(c->ssl->connection, n);
/* ... */
if (sslerr == SSL_ERROR_WANT_X509_LOOKUP)
{
    c->read->handler = ngx_ssl_handshake_handler;
    c->write->handler = ngx_ssl_handshake_handler;

    if (ngx_handle_read_event(c->read, 0) != NGX_OK) {
        return NGX_ERROR;
    }

    if (ngx_handle_write_event(c->write, 0) != NGX_OK) {
        return NGX_ERROR;
    }

    return NGX_AGAIN;
}

然後NGX_AGAIN的返回值一直往上傳遞,直到ngx_stream_core_generic_phase變為NGX_OK。然後本次的事件處理就算結束了。

事件觸發時恢復

ngx_process_events_and_timers
|-- ngx_event_expire_timers
    |-- ngx_stream_lua_sleep_handler
        |-- ngx_stream_lua_sleep_resume
            |-- ngx_stream_lua_run_thread

等到定時器超時的時候,會執行我們之前設置的ngx_stream_lua_sleep_handler,裡面會設置當前協程上下文,然後調用ngx_stream_lua_sleep_resume()

coctx = ev->data;
ctx->cur_co_ctx = coctx;
if (ctx->entered_content_phase) {
    (void) ngx_stream_lua_sleep_resume(r);
}

ngx_stream_lua_sleep_resume里調用ngx_stream_lua_run_thread恢復協程的執行。這樣就又回到了我們的Lua程式碼里。

Lua鉤子正常執行結束

接下來Lua程式碼執行完畢,lua_resume()返回,因為是協程正常結束,且沒有其他在posted隊列里的協程了,所以run_thread()直接返回NGX_OK。因此在ngx_stream_lua_finalize_request里就會實際清除fake請求。

rc = ngx_stream_lua_run_thread(vm, r, ctx, 0);

if (rc == NGX_AGAIN) {
    return ngx_stream_lua_run_posted_threads(c, vm, r, ctx, nreqs);
}

if (rc == NGX_DONE) {
    ngx_stream_lua_finalize_request(r, NGX_DONE);
    return ngx_stream_lua_run_posted_threads(c, vm, r, ctx, nreqs);
}

if (ctx->entered_content_phase) {
    ngx_stream_lua_finalize_request(r, rc);
    return NGX_DONE;
}

return rc;

裡面會調用到之前設置的cleanup函數,清理fake請求的時候調用ngx_stream_lua_request_cleanup_handler清理Lua執行緒。

cln = r->cleanup;
r->cleanup = NULL;
while (cln) {
    if (cln->handler) {
        cln->handler(cln->data);
    }

    cln = cln->next;
}

r->connection->destroyed = 1;

清理fake連接的時候調用ngx_stream_lua_ssl_cert_done。我們來看看ngx_stream_lua_ssl_cert_done裡面做了什麼。主要是設置了完成標誌,然後把前端連接的寫事件加入了ngx_posted_events隊列里。

cctx->done = 1;
ngx_post_event(c->write, &ngx_posted_events);

定時器超時事件完成之後返回到外層,處理後續的ngx_posted_events隊列事件。

(void) ngx_process_events(cycle, timer, flags);

delta = ngx_current_msec - delta;

ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
               "timer delta: %M", delta);

ngx_event_process_posted(cycle, &ngx_posted_accept_events);

if (ngx_accept_mutex_held) {
    ngx_shmtx_unlock(&ngx_accept_mutex);
}

if (delta) {
    ngx_event_expire_timers();
}

ngx_event_process_posted(cycle, &ngx_posted_events);

因為前端連接的寫事件已經設置成ngx_ssl_handshake_handler,所以會再次調用到ngx_ssl_handshake-SSL_do_handshake,這樣就再次進入了SSL狀態機,又會來到ngx_stream_lua_ssl_cert_handler中。因為是第二次進入了,且已經設置了cctx->done,所以就直接返回離開碼了,其中cctx->exit_code就是ngx.exit()時的參數,cctx->exit_code初始化時的默認值時0,但是注意到前面第一次進入ngx_stream_lua_ssl_cert_handler的時候已經將默認值設為1了。

if (cctx && cctx->entered_cert_handler) {
    /* not the first time */

    if (cctx->done) {
        ngx_log_debug1(NGX_LOG_DEBUG_STREAM, c->log, 0,
                       "stream lua_certificate_by_lua:"
                       " cert cb exit code: %d",
                       cctx->exit_code);

        dd("lua ssl cert done, finally");
        return cctx->exit_code;
    }

    return -1;
}

接下來,回到了tls_post_process_client_hello()繼續後面的握手流程了。

Lua鉤子內出錯的情況

出錯的流程跟正常結束類似,只不過返回值不一樣。ngx.exit()的實現如下

ngx.exit = function (rc)
    local err = get_string_buf(ERR_BUF_SIZE)
    local errlen = get_size_ptr()
    local r = get_request()
    if r == nil then
        error("no request found")
    end
    errlen[0] = ERR_BUF_SIZE
    rc = ngx_lua_ffi_exit(r, rc, err, errlen)
    if rc == 0 then
        -- print("yielding...")
        return co_yield()
    end
    if rc == FFI_DONE then
        return
    end
    error(ffi_string(err, errlen[0]), 2)
end

裡面會調用ffi函數ngx_stream_lua_ffi_exit(),在其中設置ctx->exit_code,然後返回NGX_OK

if (ctx->context & (NGX_STREAM_LUA_CONTEXT_SSL_CERT
                    | NGX_STREAM_LUA_CONTEXT_SSL_CLIENT_HELLO ))
{
    ctx->exit_code = status;
    ctx->exited = 1;
    return NGX_OK;
}

回到ngx.exit()函數之後,就調用原生的coroutine.yield(),回到我們的主執行緒run_thread()之後,因為設置了ctx->exited會調用ngx_stream_lua_handle_exit返回

rv = lua_resume(orig_coctx->co, nrets);
switch (rv) {
    case LUA_YIELD:
        if (ctx->exited) {
            return ngx_stream_lua_handle_exit(L, r, ctx);
        }
}

ngx_stream_lua_handle_exit()裡面調用ngx_stream_lua_request_cleanup清理執行緒。

ctx->cur_co_ctx->co_status = NGX_STREAM_LUA_CO_DEAD;
ngx_stream_lua_request_cleanup(ctx, 0);
return ctx->exit_code;

然後返回到sleep_resume,此時rcctx->exit_code,即ngx.ERROR,接下來跟正常結束時一樣也是結束我們的請求

rc = ngx_stream_lua_run_thread(L, r, ctx, 0);
...;
if (ctx->entered_content_phase) {
    ngx_stream_lua_finalize_request(r, rc);
    return NGX_DONE;
}
return rc;

因為是fake請求,ngx_stream_lua_finalize_request調用ngx_stream_lua_finalize_fake_request,裡面將cctx->exit_code設為0。

if (rc == NGX_ERROR || rc >= NGX_STREAM_BAD_REQUEST) {
    if (r->connection->ssl) {
        ssl_conn = r->connection->ssl->connection;
        if (ssl_conn) {
            c = ngx_ssl_get_connection(ssl_conn);
            if (c && c->ssl) {
                cctx = ngx_stream_lua_ssl_get_ctx(c->ssl->connection);
                if (cctx != NULL) {
                    cctx->exit_code = 0;
                }
            }
        }
    }
    ngx_stream_lua_close_fake_request(r);
    return;
}

在清理fake請求的時候調用ngx_stream_lua_request_cleanup_handler清理Lua執行緒。在清理fake連接的時候會觸發ngx_stream_lua_ssl_cert_done,跟正常完成時一樣,也是設置完成標誌,然後把前端連接的寫事件加入了ngx_posted_events隊列里。

cctx->done = 1;
ngx_post_event(c->write, &ngx_posted_events);

到此定時器的事件就結束了,開始處理後續的posted隊列事件。同樣地,也會再次調用ngx_ssl_handshake_handler最終調到到ngx_stream_lua_ssl_cert_handler中。因為是第二次進入了,且已經設置了cctx->done,所以就直接返回離開碼了,而本次因為是出錯cctx->exit_code的值是0.

返回到OpenSSL之後,一路往上傳遞錯誤碼。。。

int rv = s->cert->cert_cb(s, s->cert->cert_cb_arg);
if (rv == 0) {
    goto err;
}
err:
return WORK_ERROR;

最終,SSL_do_handshake返回錯誤值,結束SSL握手。

n = SSL_do_handshake(c->ssl->connection);
sslerr = SSL_get_error(c->ssl->connection, n);
return NGX_ERROR;

總結

我們本篇是以一個定時器為例子,對於socket I/O等待其實也是類似的流程。只不過觸發事件由定時器超時變成了相應的fd的讀寫事件,協程的恢復由定時器時的直接恢復變成了完成本次I/O任務(或者出錯)之後恢復協程。