前面一篇文章介紹了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;
重點是最後的兩個函式,它們分別初始化registry
和globals
。這個兩個函式都不算太長,讓我們來完整看下它們做了些什麼。
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_accept
及SSL_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
,此時rc
為ctx->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任務(或者出錯)之後恢復協程。