前面一篇文章介紹了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;
}

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

初始化階段

初始化階段的流程比較簡單:配置解析階段會讀取配置檔案中的程式碼塊進行解析儲存,然後建立Lua程式碼的key,這個key是用於後面將程式碼cache到登錄檔的。配置合併階段,主要是合併配置項,然後設定cert_cb回撥。配置後處理階段,主要工作是初始化Lua VM,包括建立登錄檔項、建立全域性表項ngx、替換coroutine介面。

連線階段

連線階段因為涉及新連線進入鉤子、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任務(或者出錯)之後恢復協程。