tls: prevent engine hang from spinning TLS read/write retry loop#12070
tls: prevent engine hang from spinning TLS read/write retry loop#12070shaq918 wants to merge 1 commit into
Conversation
When a TLS connection's underlying TCP socket continuously reports EPOLLIN but SSL_read() returns SSL_ERROR_WANT_READ (no complete TLS records available), flb_tls_net_read_async() enters a tight spin loop: 1. SSL_read() → read(fd) → EAGAIN 2. OpenSSL returns SSL_ERROR_WANT_READ → FLB_TLS_WANT_READ 3. io_tls_event_switch() is a no-op (event already registered for READ) 4. flb_coro_yield() yields, engine sees EPOLLIN, resumes immediately 5. goto retry_read → back to step 1 This monopolizes the main engine thread (flb-pipeline), starving all other event processing: output flushes never execute, scheduler timers don't fire, and new connections can't be accepted. The entire output pipeline hangs while the process appears healthy. Observed in production with Fluent Bit v3.2.2 where a dead forward input TLS connection (TCP ESTABLISHED, rx_queue=0, no data for 3.6 days) caused the engine thread to spin at ~8,500 failed read() calls/second. The output_proc_records_total counter froze, but output_errors_total remained 0. Same pattern confirmed on a second independent node with a different dead connection (47 days idle). Validated by killing the dead TCP connections on the live system: output_proc_records immediately jumped from 9,123,353 to 9,264,379 (+141,026 records flushed) and the engine thread resumed normal operation — proving the dead TLS connections were the sole cause. Fix: Add FLB_TLS_WANT_READ_MAX_RETRIES (800) to cap consecutive WANT_READ/WANT_WRITE iterations with no progress in both flb_tls_net_read_async() and flb_tls_net_write_async(). When the limit is exceeded, return -1 to close the stale connection and allow the engine thread to resume normal event processing. Signed-off-by: shaq918 <shaq918@users.noreply.github.com> Fixes: fluent#9927
📝 WalkthroughWalkthroughAdds a new ChangesTLS Async Retry Timeout Handling
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Coroutine
participant TLSAsyncIO as flb_tls_net_read_async/write_async
participant EventLoop
Coroutine->>TLSAsyncIO: perform TLS read/write
TLSAsyncIO->>TLSAsyncIO: attempt operation
alt WANT_READ/WANT_WRITE returned
TLSAsyncIO->>TLSAsyncIO: increment retry counter
alt retries <= FLB_TLS_WANT_READ_MAX_RETRIES
TLSAsyncIO->>EventLoop: yield and wait for event
EventLoop-->>Coroutine: resume coroutine
else retries exceeded
TLSAsyncIO->>TLSAsyncIO: set net_error = ETIMEDOUT, clear coroutine
TLSAsyncIO->>EventLoop: restore backed-up event registration
TLSAsyncIO-->>Coroutine: return -1
end
else operation completes
TLSAsyncIO-->>Coroutine: return bytes read/written
end
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 54019a9639
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
| else if (ret == FLB_TLS_WANT_WRITE) { | ||
| /* Progress on a different axis, reset the read retry counter */ | ||
| want_read_retries = 0; |
There was a problem hiding this comment.
Cap WANT_WRITE retries during TLS reads
When SSL_read() returns FLB_TLS_WANT_WRITE, this branch resets the only retry counter and then retries without any limit. For a stalled TLS read that needs the socket write side, or alternates WANT_READ/WANT_WRITE without returning bytes, the coroutine can still bounce through flb_coro_yield() indefinitely and monopolize the engine thread; count WANT_WRITE as a no-progress retry too, or reset only after a successful read.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/tls/flb_tls.c (1)
540-631: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winReset
want_retriesafter forward progress.
tls->api->net_write()can return a positive partial count, and this loop keeps sending the remaining bytes. Without resettingwant_retrieson success, a long but healthy transfer can accumulate more than 800 WANT_READ/WANT_WRITE events over the same send and get closed as stale.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/tls/flb_tls.c` around lines 540 - 631, The retry counter in flb_tls_net_write can incorrectly carry across successful partial writes, causing healthy long sends to be treated as stale; update the retry handling in the flb_tls_net_write loop so want_retries is reset after any positive progress from tls->api->net_write, while keeping the existing retry limits and event switching behavior intact.
🧹 Nitpick comments (1)
include/fluent-bit/tls/flb_tls.h (1)
38-43: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConstant name is read-specific but reused on the write path.
FLB_TLS_WANT_READ_MAX_RETRIESis also used to boundWANT_WRITE/write retries inflb_tls_net_write_async, which makes the name misleading at those call sites. Consider a neutral name such asFLB_TLS_WANT_IO_MAX_RETRIES.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@include/fluent-bit/tls/flb_tls.h` around lines 38 - 43, The retry limit constant name is too read-specific for a value that is also used by write retry logic, so rename FLB_TLS_WANT_READ_MAX_RETRIES in flb_tls.h to a neutral I/O-oriented symbol and update all call sites such as flb_tls_net_write_async and any related TLS retry checks to use the new name consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/tls/flb_tls.c`:
- Around line 540-631: The retry counter in flb_tls_net_write can incorrectly
carry across successful partial writes, causing healthy long sends to be treated
as stale; update the retry handling in the flb_tls_net_write loop so
want_retries is reset after any positive progress from tls->api->net_write,
while keeping the existing retry limits and event switching behavior intact.
---
Nitpick comments:
In `@include/fluent-bit/tls/flb_tls.h`:
- Around line 38-43: The retry limit constant name is too read-specific for a
value that is also used by write retry logic, so rename
FLB_TLS_WANT_READ_MAX_RETRIES in flb_tls.h to a neutral I/O-oriented symbol and
update all call sites such as flb_tls_net_write_async and any related TLS retry
checks to use the new name consistently.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 36038539-77bc-47ed-a991-7fc5abd58c12
📒 Files selected for processing (2)
include/fluent-bit/tls/flb_tls.hsrc/tls/flb_tls.c
|
Note: PR #11547 addresses the same root cause (infinite WANT_READ/WANT_WRITE retry loops) but only fixes the synchronous paths ( Both PRs are complementary and needed for full coverage. |
Summary
Fixes #9927 — Fluent Bit silently hangs (stops forwarding data) while appearing healthy.
When a TLS forward input connection dies (peer disconnects ungracefully),
flb_tls_net_read_async()enters a tight spin loop that monopolizes the main engine thread and starves all output processing.Root Cause
In
flb_tls_net_read_async()(src/tls/flb_tls.c):rx_queue=0SSL_read()->read(fd)->EAGAIN-> OpenSSL returnsSSL_ERROR_WANT_READio_tls_event_switch()is a no-op because the event is already registered forMK_EVENT_READflb_coro_yield()yields, but epoll immediately resumes the coroutine (level-triggered, event never consumed)goto retry_read-> step 2 -> tight spin, ~8,500 iterations/secondThis monopolizes the
flb-pipelinethread. All other event processing is starved: output flushes never execute, scheduler timers don't fire, chunks stay permanently busy.Fix
Add
FLB_TLS_WANT_READ_MAX_RETRIES(800) to cap consecutiveWANT_READ/WANT_WRITEiterations with no progress in bothflb_tls_net_read_async()andflb_tls_net_write_async(). When exceeded, return-1to close the stale connection and unblock the engine thread.Evidence from Production (Fluent Bit v3.2.2, Ubuntu 22.04, OpenSSL 3.0.2)
Strace on stuck flb-pipeline thread (the main engine thread):
17,029 failed read() calls in 2 seconds — 100% EAGAIN, zero useful work.
The spinning fd is a dead TCP socket:
Output counter frozen:
Storage API — chunks stuck busy:
{"tail.0": {"chunks": {"total":4, "busy":3}}}Live Validation
Killing the dead TCP connections (simulating what this fix does) immediately recovered the pipeline without a restart:
Before (stuck):
After killing dead connections:
Same pattern confirmed on a second independent node with a different dead connection (47 days idle, different peer IP/port). Multiple dead connections can stack — we had to close 4-5 before the engine fully recovered.
Files Changed
include/fluent-bit/tls/flb_tls.h— addFLB_TLS_WANT_READ_MAX_RETRIESconstant (800)src/tls/flb_tls.c— add retry counter toflb_tls_net_read_async()andflb_tls_net_write_async()Summary by CodeRabbit