fix(tracer): prevent stale ctx.tracing crash on HTTPS keepalive connections#13232
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes a crash in APISIX comprehensive tracing on HTTPS keepalive connections by ensuring tracing state in ngx.ctx is re-initialized when the pooled table has been cleared, and adds a regression test to cover consecutive HTTPS requests with tracing enabled.
Changes:
- Make
tracer.start()reinitialize tracing whenctx.tracing.spansis missing (stale/cleared pooled table). - Make
tracer.release()tolerant of partially-cleared tracing state by guarding span iteration/release. - Add a node test that performs consecutive HTTPS requests over a reused connection while tracing is enabled.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
apisix/tracer.lua |
Hardens tracer init/release logic against cleared pooled state in ngx.ctx across HTTPS keepalive requests. |
t/node/tracer.t |
Adds regression coverage for consecutive HTTPS requests with apisix.tracing: true. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Does this fix apply to HTTP/2 in cases where the second HTTP request starts before the first one ends, causing the two requests to share the same ctx.tracing? |
i've added the test case here to address your issue. after doing some digging, i found that in http2 requests, ctx.tracing won't be shared across different requests since each request would init a new one. hence could u elaborate more on your issue or if there is any reproduction steps? |
|
The PR is on the right track; please fix the failed CI. |
We recently encountered an similar issue in our online environment involving ngx.ctx during the SSL phase and request processing phases. Here is an example to demonstrate the problem: nginx.conf: ssl_certificate_by_lua_block {
ngx.ctx.tracing={}
ngx.log(ngx.WARN, "id: ", tostring(ngx.ctx.tracing))
}
location /slow {
content_by_lua_block {
ngx.log(ngx.WARN, "id(slow): ", tostring(ngx.ctx.tracing))
table.insert(ngx.ctx.tracing, "slow_1")
ngx.sleep(1)
table.insert(ngx.ctx.tracing, "slow_2")
ngx.say("slow")
}
}
location /fast {
content_by_lua_block {
ngx.log(ngx.WARN, "id(fast): ", tostring(ngx.ctx.tracing))
table.insert(ngx.ctx.tracing, "fast_1")
ngx.say("fast")
}
}
log_by_lua_block {
ngx.log(ngx.WARN, "data: ", table.concat(ngx.ctx.tracing, ", "), ", id:", tostring(ngx.ctx.tracing))
ngx.ctx.tracing=nil
ngx.log(ngx.WARN, "nil?(after set to nil): ", ngx.ctx.tracing==nil)
ngx.log(ngx.WARN, "data(after set to nil): ", table.concat(ngx.ctx.tracing, ", "))
}Run the command bellow to process two requests concurrently within the same connection: Error Log Output:(Prefix timestamps and metadata removed) Key Observations from the Logs:
Based on the implementation relate to ngx.ctx get_ctx_table, the ngx.ctx accessed during the SSL phase acts as the metatable for the ngx.ctx accessed during the request processing phase. In the example above:
In this specific issue, the PR fixes the crash, the tracing data still interference across concurrent requests. |
WDYT @Baoyuantop @membphis @nic-6443 ? I can confirm that this PR solves the crash issue, but I am not sure how to write test case to confirm no data interference between both requests. One idea is to check span using opentelemetry but ig we can do it in separate PR? i'm working on a solution right now just want to have ur guys opinion. |
|
|
I have carefully read the problem analysis and fix suggestions provided by @shreemaan-abhishek, and I agree with his views. It is the correct approach to separate the tracers for SSL and HTTP, because the same SSL connection can be shared by multiple HTTP requests, and forcibly linking them together will lead to data disorder issues. |
|
Thanks for the thorough and detailed explanation @shreemaan-abhishek, and I totally agree with your view and have already implemented the fix based on your suggestion. The changes made:
As you mentioned, one consequence of this fix is that the SSL-handshake span is now its own standalone trace rather than being stitched into the HTTP request's trace. This also means For the tests, I have added:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Description
When
apisix.tracingis enabled, the core tracer instruments every request phase — includingssl_client_hello_phase— by allocating a tracing table fromlua-tablepooland storing it inngx.ctx.tracing.On HTTPS/HTTP2 keepalive connections, OpenResty creates the SSL-phase
ngx.ctxonce per TLS connection and uses it as the__indexmetatable for each subsequent HTTP request'sngx.ctx. This means readingctx.tracingon a per-request ctx falls through to the SSL-phase ctx'stracingfield — all requests on the same connection share the same tracing table.The bug occurs in the following sequence:
ssl_client_hello_phasecallstracer.start(), which writesctx.tracingonto the SSL-phase ctx.tracer.release(), returning the tracing table to the pool.lua-tablepoolzeroes all fields viatable.clear(), butctx.tracing = nilonly writes onto the per-request ctx — the SSL-phase ctx'stracingfield is untouched (no__newindexpropagation).ctx.tracingvia__indexand crashes attable.insert(tracing.spans, self)inspan.new()becausespansisnil.This fix addresses the root cause by severing the metatable inheritance using
rawget/rawset:tracer.start(): Usesrawget(ctx, "tracing")instead ofctx.tracing.rawgetbypasses__index, so each per-request ctx seesniland initialises its own fresh tracing table viarawset. In the SSL phase,ctxis the SSL-phase table directly sorawgetbehaves identically to a plain read — no change in behaviour for that phase.tracer.release(): Usesrawset(ctx, "tracing", nil)to clear the per-request ctx's own field directly, bypassing any metatable.ssl_client_hello_phaseininit.lua:tracer.release(ngx_ctx)is now called at all exit points ofssl_client_hello_phase. Withrawgetin place, the SSL-phase tracing table would otherwise never be picked up or released by any HTTP request phase and would leak for the lifetime of the TLS connection. Making it explicit and SSL-scoped is the correct shape.One consequence of this fix: the SSL-handshake span is now its own standalone trace rather than being stitched into the HTTP request's trace. Stitching across SSL and HTTP phases requires propagating span context IDs (traceparent), not sharing the live tracing object — this is left as a separate change.
Which issue(s) this PR fixes:
Fixes #13200
Checklist
t/node/tracer.t,t/plugin/opentelemetry6.t)