[
https://issues.apache.org/jira/browse/THRIFT-6244?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Jens Geyer resolved THRIFT-6244.
--------------------------------
Assignee: Jens Geyer
Resolution: Fixed
> TNonblockingServerTest intermittently crashes or hangs in
> bad_alloc_does_not_end_the_process
> --------------------------------------------------------------------------------------------
>
> Key: THRIFT-6244
> URL: https://issues.apache.org/jira/browse/THRIFT-6244
> Project: Thrift
> Issue Type: Bug
> Components: C++ - Library
> Reporter: Jens Geyer
> Assignee: Jens Geyer
> Priority: Minor
> Fix For: 0.25.0
>
> Time Spent: 20m
> Remaining Estimate: 0h
>
> {{TNonblockingServerTest}} fails intermittently on AppVeyor, always in the
> case {{bad_alloc_does_not_end_the_process}}: either the test binary crashes
> (ctest reports SEGFAULT) or it hangs until the 300 s ctest timeout.
> The case was added with
> [eda9f8d71|https://github.com/apache/thrift/commit/eda9f8d7171d88082ae5f00642411c11c92ba8a4],
> and the first AppVeyor build that contained it (0.25.0.10466) was also the
> first failure. Since then 11 of 229 jobs have failed (MSVC2022: 8 crashes and
> 2 timeouts out of 192 jobs; MinGW: 1 timeout out of 37), and 11 of 39 builds
> went red because of it. A search of all failed AppVeyor job logs since
> 2025-07-22 finds no earlier failure of this test.
> Example jobs:
> * crash:
> [0.25.0.10469|https://ci.appveyor.com/project/ApacheSoftwareFoundation/thrift/builds/54684673/job/fw4ai49opu5j66kw]
> * timeout:
> [0.25.0.10466|https://ci.appveyor.com/project/ApacheSoftwareFoundation/thrift/builds/54684499/job/ueslnq02ob7c29w7]
> h2. Cause 1: the fixture destroys the server while a thread-pool task is
> still running (crash)
> In the crashing runs, the server output of this case ends like this
> (timestamps removed):
> {noformat}
> TNonblockingServer: IO thread #0 entering loop...
> TNonblockingServer: IO thread #0 run() done!
> TNonblocking: join done for IO thread #0
> TNonblockingServer: process() exception: class std::bad_alloc: bad allocation
> TNonblockingServer: failed to notifyIOThread, closing.
> {noformat}
> The worker handling the deliberately failed call reaches its catch handler
> only after the fixture has already stopped the server:
> * {{FailsFirstCallProcessor}} sets its flag and throws. Meanwhile the second
> worker serves {{canCommunicate()}}, the test body returns, and the fixture is
> torn down.
> * The fixture destructor calls {{stop()}} on the server and {{join()}} on the
> serve thread, but never stops the {{ThreadManager}}.
> * The fixture members are then destroyed in reverse declaration order, the
> server before the thread manager. The {{TNonblockingServer}} destructor
> closes and deletes every connection and the IO threads, including the
> notification pipe; only afterwards does the {{ThreadManager}} destructor wait
> for its workers.
> * The pool task holds a raw pointer to its {{TConnection}}. The task that is
> still unwinding then calls {{notifyIOThread()}} on the deleted connection
> and, when that fails, {{close()}}.
> Four passing runs show the same late ordering, with {{run() done!}} logged
> before the {{bad_alloc}} line; they finished just in time.
> h2. Cause 2: the injected failure is not tied to the call meant to receive it
> (timeout)
> {{FailsFirstCallProcessor}} fails whichever call reaches {{process()}} first,
> tracked in a plain {{bool}} that both pool workers read and write. The call
> that consumes the failure gets no reply. If that is the {{addString}} call
> from {{canCommunicate()}}, its client, which has no receive timeout, blocks
> until ctest kills the test. The missing reply itself is the server behaviour
> tracked in THRIFT-6216; fixing that would turn the hang into a failed check,
> but would not remove the race. Two variants have been observed:
> * Windows: both workers saw the flag unset, and the log shows the
> {{bad_alloc}} line twice.
> * Linux, running the unmodified test under ASan with CPU load from 8 parallel
> loops: the order was inverted. The call meant to fail was served normally,
> its reply hitting the already closed connection ("Broken pipe"), and the call
> from {{canCommunicate()}} received the failure. 2 of 1200 runs hung; 400
> sequential runs all passed. The crash did not reproduce on Linux.
> h2. Proposed fix (test only)
> Both parts are needed, because each one closes only one of the two symptoms:
> # In the fixture destructor, stop the {{ThreadManager}} after joining the
> serve thread and before the server is destroyed. A task still in flight then
> completes while its connection and the notification pipe still exist. On its
> own this does not help with the timeout, because the test body hangs and
> teardown is never reached.
> # Tie the injected failure to the call meant to receive it: for example, use
> a {{std::atomic<bool>}} and call {{canCommunicate()}} only after that call
> has consumed the failure, signalled for instance through a {{Monitor}} right
> before the throw. On its own this does not help with the crash, because the
> failing task can still run into teardown.
> Optionally, a receive timeout on the socket in {{canCommunicate()}} would
> make a regression fail within seconds instead of after 300 s.
> Out of scope: {{TNonblockingServer::stop()}} and the server destructor do not
> wait for tasks still running in the thread pool, so an application currently
> has to stop the server, join the serve thread and stop the {{ThreadManager}}
> before destroying the server. Whether the server itself should wait is a
> separate question.
> _Drafted with AI assistance (Claude Opus 5)._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)