<div dir="ltr"><div dir="auto">Good point, I hadn't considered that it was regular common ref count 0 dealloc chaining.  The processes unfortunately didn't have faulthandler enabled so there wasn't much info from where in the python code it happened (now fixed).  I'll see if anything looks particularly unusual next time I hear of such a report.</div><div dir="auto"><br></div><div>-G</div></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Wed, Mar 27, 2019, 5:38 PM Tim Peters <<a href="mailto:tim.peters@gmail.com" target="_blank">tim.peters@gmail.com</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">[Gregory P. Smith <<a href="mailto:greg@krypto.org" rel="noreferrer" target="_blank">greg@krypto.org</a>>]<br>
> ...<br>
> A situation came up the other day where I believe this could've helped.<br>
><br>
> Scenario (admittedly not one most environments run into): A Python process<br>
> with a C++ extension module implementing a threaded server (threads<br>
> spawned by C++) that could call back into CPython within server request<br>
> handlers.  (ie: how all threaded servers tend to work regardless of core<br>
> loop implementation language)<br>
><br>
> Python code in the application had done something (unknown to me, I<br>
> didn't dive into their code) that built up large enough presumably nested<br>
> or recursive data structures that the garbage collector, when triggered,<br>
> would wind up in very deep recursion.  This caused a stack overflow<br>
>  as the C++ spawned threads were only being given a 256k stack (to<br>
> conserve virtual address space - there can potentially be a _ton_ of<br>
> threads in this code).<br>
><br>
> That had a C++ stack trace 1000+ levels deep repeating the pattern of<br>
><br>
> ...<br>
>     @     0x564d59bd21de         32  func_dealloc<br>
>     @     0x564d59bce0c1         32  cell_dealloc<br>
>     @     0x564d5839db41         48  tupledealloc<br>
>     @     0x564d59bd21de         32  func_dealloc<br>
>     @     0x564d59bce0c1         32  cell_dealloc<br>
>     @     0x564d5839db41         48  tupledealloc<br>
> ...<br>
><br>
> If our gc were done on a thread of its own spawned by Python, with a typical<br>
> normal larger default stack size (8mb) this would've been less likely<br>
> to trigger a crash (though obviously still possible if the recursion is linear).<br>
<br>
Just noting that gc is probably a red herring in that scenario.  gc<br>
(cleverly!) determines the set of trash objects without needing any<br>
recursive graph crawls.  And gc doesn't deallocate anything - not<br>
directly.  It simply applies Py_DECREF to trash objects, once for each<br>
PyObject* found pointing to them.  _If_ deallocation occurs as a<br>
result, it's the same as if the user had done `del` on the appropriate<br>
object manually.  The recursion then occurs in the chain of<br>
XXX_dealloc calls, which in turn do more Py_DECREFs of their own, and<br>
which have nothing in particular to do with gc.  Approximately the<br>
same stack depth would be hit in a Python built without gc if the user<br>
did the same sequence of `del`s by hand to break trash cycles.<br>
<br>
The good news is that the "trashcan" mechanism - which predates gc -<br>
was introduced to limit call depth in the presence of some potentially<br>
unbounded chains of dealloc calls.  So teach trashcan about the<br>
pattern here, and the stack depth problem goes away, with or without<br>
gc.<br>
<br>
The bad news is that the traschcan mechanism is excruciating, a<br>
long-time source of subtle bugs of its own :-(<br>
<br>
Note:  without actual code to run, it's possible that trashcan is<br>
_already_ trying to limit stack depth in this specific case, but the<br>
stack size is too small to accommodate the largest depth of recursive<br>
calls trashcan is willing to allow before interfering.  Or some bug<br>
snuck into trashcan, or its invoking code, that causes it to fail to<br>
work as intended due to some unknown detail of the code above.<br>
There's just no way to guess without code to provoke it.<br>
<br>
<br>
> Another take away from this is that it appears possible to cause our gc<br>
> to go into linear recursion.<br>
<br>
As above, it's probably not gc, but deallocation as a side effect of<br>
Py_DECREF dropping a refcount to 0.  gc is just one way Py_DECREF can<br>
get invoked.<br>
</blockquote></div>