[BUG] A send during WaitBackgroundThreadExit starts a second background thread, and teardown races it
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 38/100
- Tipo di issue
- Bug
- Chiarezza
- Abbastanza chiara
- Stato di attività
- Attiva
- Stack tecnologico
- cmake, cpp
- Ambito
- networking
Direzione di ricerca
Start in http_client_curl.cc at MaybeSpawnBackgroundThread(), WaitBackgroundThreadExit(), the destructor loop around lines 322-342, and the self-detach path near line 654; compare these lifecycle paths with ext/test/http/curl_http_test.cc. Run the named HTTP curl tests with thread instrumentation and ThreadSanitizer, using the gated AfterWait ordering described. Done means the wait leaves no client background thread running and teardown produces no curl_multi_cleanup race.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Describe your environment
WaitBackgroundThreadExit()came in with #3198 and first shipped in v1.19.0. The re-swap loop it is missing has been in the destructor since #1413 and v1.5.0.mainat489cd823still has both shapes.- Ubuntu 24.04, gcc 14, libcurl 8.14.1, CMake, built with
OTELCPP_WITH_THREAD_INSTRUMENTATION_PREVIEW=ON. That flag is what makes the ordering below deterministic instead of timed; the code it exercises is not behind it and is not platform specific.
Steps to reproduce
- Give an
ext::http::client::curl::HttpClientaThreadInstrumentationwhoseAfterWait()blocks until the test releases it.AfterWaitis called athttp_client_curl.cc:525and holds no lock.OnEndwill not do for this: it runs at:648, inside the guard taken at:612, which is the mutex the spawn needs, so blocking there blocks the spawn instead of letting it through. - Call
MaybeSpawnBackgroundThread()and wait until the thread is parked inAfterWait. - From another thread, call
WaitBackgroundThreadExit(). It swapsbackground_thread_out at:723, releases the lock at:724, and blocks in the join at:729because the thread it took is parked. - Send a request.
Session::SendRequestcallsMaybeSpawnBackgroundThread()at:258, it finds a null member, and a second thread starts. Point the request at an address that does not answer,https://192.0.2.0:19000/get/asSendGetRequestAsyncTimeoutalready does, so the new thread has a session to service rather than nothing to do. - Release the parked thread, join the caller of
WaitBackgroundThreadExit(), and count the background threads that have started and not ended.
What is the expected behavior?
Once WaitBackgroundThreadExit() returns, no background thread of that client is running. The destructor already behaves that way. Lines :322 to :342 swap and join in a loop until a swap comes back empty, so a thread that appears during teardown is collected on the next turn.
What is the actual behavior?
The wait joins the thread it swapped out and returns whether or not another has appeared:
719: is_shutdown_.store(true, ...);
722: std::lock_guard<std::mutex> lock_guard{background_thread_m_};
723: background_thread.swap(background_thread_); <- member is null from here
724: } <- lock released
729: background_thread->join(); <- joins only what it swapped out
731: is_shutdown_.store(false, ...);
MaybeSpawnBackgroundThread() takes the same mutex at :471 and then tests background_thread_ at :472 and nothing else. From :724 to :729 that member is null, so the test passes and a thread starts. is_shutdown_ is true for the whole span and the spawn never reads it.
Nor does the new thread simply retire again. With a session queued, doAddSessions() at :629 sets still_running and it stays in the loop. Five runs out of five, with the ordering forced by the gate rather than by timing: two threads started, and one of them was still in its loop at the moment the wait returned.
Teardown then races it. Finishing the session, calling FinishAllSessions() and then destroying the client, which is what a caller would do next, gives ThreadSanitizer this, three runs out of three:
SUMMARY: ThreadSanitizer: data race in curl_multi_cleanup
Write of size 8 by main thread (mutexes: write M0):
curl_multi_cleanup <- from ~HttpClient
Previous read of size 8 by thread T4:
libcurl.so.4
Location is file descriptor 5 created by main thread
Thread T4 created by main thread at:
Session::SendRequest(...) http_client_curl.cc:258
T4 is the thread the send started. All 16 reports in that run have the main thread on one side and T4 on the other, and 13 of them name ~HttpClient.
Additional context
The race needs both halves, so here is the grid rather than the one cell that fails. Each row is the same TSan build, run on its own:
| scenario | window | session in flight | races |
|---|---|---|---|
SendGetRequestAsyncTimeout, unchanged |
no | yes, same unreachable address | 0, 3 of 3 |
ElegantQuitQuick, unchanged |
no | yes, and it destroys with a thread live | 0, 3 of 3 |
| the window held open, with no session queued | yes | no | 0 |
| the recipe above | yes | yes | 16, 16, 3 |
Two existing cases already destroy a client while a background thread runs, and they are clean. Neither the window alone nor the work alone produces anything.
What I have not instrumented is how the second thread gets past the destructor's join loop. My reading is the self-detach at :654: the thread retires, finds its own handle in background_thread_, detaches and clears it, and the loop at :322 then swaps out an empty pointer and breaks. That is a reading of the code, not a measurement, and I would rather hand it over as one.
Nothing in this repository calls WaitBackgroundThreadExit() outside ext/test/http/curl_http_test.cc, where it is called six times, so no shipped code path reaches this today. It is public API.
This is not #4402. That one parks the single IO thread from inside a handler. This is the opposite: a second IO thread where the design allows one.
None of #4395, #4405 or #4406 covers it. They change the loop body and the retry path, and leave the swap at :723 and the test at :472 as they are.
#4664 is the other open report on this loop, filed the same day: the CURLMcode from curl_multi_poll at :513 is stored and never read. Different defect, same function.
I would like to take it. The destructor's loop is the shape that already works here; the other option is for MaybeSpawnBackgroundThread() to refuse while is_shutdown_ is set. The second changes what a send does during shutdown, so it is worth a word from you before I write either one.
Tip: React with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding +1 or me too, to help us triage it. Learn more here.
- Lingua principale
- C++
- Stelle
- 1.4k
- Fork
- 647
- Merge medio
- 1g 10h
- PR unite (30g)
- 73
Preparare l'ambiente
Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di open-telemetry/opentelemetry-cpp
-
[CI] Add Ubuntu 26.04 runners to the CI workflowForse già presa @deodattap l’ha presa 9 giorni fa. Apertatriage/accepted
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
open-telemetry/opentelemetry-cpp#4596 · 2 commenti · 1 reazione ·
I maintainer di solito rispondono entro 1 giorno
-
[BUG] Resource::Create() throws bad_variant_access if process.executable.name isn't a stringForse già presa @ryux1 l’ha presa 31 giorni fa. Apertabug help wanted triage/accepted
Difficoltà 2/5 1-3 ore Idoneità per principianti 84/100
open-telemetry/opentelemetry-cpp#4535 · 1 commento · 2 reazioni ·
I maintainer di solito rispondono entro 1 giorno
-
[BUG] OnResponse() can call std::terminate() when the response body fails to parse as JSON/protobufForse già presa @YuEfSaEDU l’ha presa 22 giorni fa. Apertabug help wanted triage/accepted
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
open-telemetry/opentelemetry-cpp#4534 · 2 commenti · 1 reazione ·
I maintainer di solito rispondono entro 1 giorno
-
[BUG] ETW Properties::to_vector doubles the result and reads past a string_viewForse già presa @Tyagiquamar l’ha presa 7 giorni fa. Apertaneeds-triage Stale
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
open-telemetry/opentelemetry-cpp#4347 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
bug Stale triage/accepted
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 62/100
open-telemetry/opentelemetry-cpp#3109 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di open-telemetry/opentelemetry-cpp
Issue simili
-
Difficoltà 2/5 Meno di un'ora Idoneità per principianti 72/100
ashhart/TensorFold#535 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 66/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
I maintainer di solito rispondono entro 1 giorno
-
agent:Windows bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
I maintainer di solito rispondono entro 1 giorno