Skip to content

Can diagnose=True prevent process exit by keeping traceback references alive? #1434

@akozlu

Description

@akozlu

Description

Looking for confirmation if I get the following behavior right:

When using diagnose=True, the loguru exception formatting keeps references to the traceback frames and keeps the variables alive.
Is it then possible that this prevents garbage collection of objects referenced in those frames and hangs the process indefinitely, IFF the referenced objects manage background threads (SSHTunnel, connection pools) and they do not have atexit defined in their cleanup?

Minimal Reproducible Example

  import atexit # would be the workaround
  import threading                                                                                                                                                                                               
  from loguru import logger                                                                                                                                                                                      
                                                                                                                                                                                                                 
  class ResourceWithThread:                                                                                                                                                                                      
      """Simulates a resource that starts a non-daemon thread (e.g., SSH tunnel)."""                                                                                                                             
      def __init__(self):                                                                                                                                                                                        
          self._stop = threading.Event()                                                                                                                                                                         
          self._thread = threading.Thread(target=self._run, daemon=False)                                                                                                                                        
          self._thread.start()                                                                                                                                                                                   
                                                                                                                                                                                                                 
      def _run(self):                                                                                                                                                                                            
          self._stop.wait()  # Blocks until stop() is called                                                                                                                                                     
                                                                                                                                                                                                                 
      def stop(self):                                                                                                                                                                                            
          self._stop.set()                                                                                                                                                                                       
          self._thread.join(timeout=1)                                                                                                                                                                           
                                                                                                                                                                                                                 
  def main():                                                                                                                                                                                                    
      logger.remove()                                                                                                                                                                                            
      logger.add(                                                                                                                                                                                                
          sink=lambda msg: print(msg, end=""),                                                                                                                                                                   
          diagnose=True,  # Setting to False allows clean exit                                                                                                                                                   
      )                                                                                                                                                                                                          
                                                                                                                                                                                                                 
      resource = ResourceWithThread()                                                                                                                                                                            
                                                                                                                                                                                                                 
      try:                                                                                                                                                                                                       
          raise RuntimeError("Something went wrong")                                                                                                                                                             
      except Exception:                                                                                                                                                                                          
          logger.exception("Error occurred")                                                                                                                                                                     
                                                                                                                                                                                                                 
      # Even though we're done, the process hangs because:                                                                                                                                                       
      # 1. diagnose=True keeps the traceback alive for variable inspection                                                                                                                                       
      # 2. The traceback references the frame where `resource` is a local                                                                                                                                        
      # 3. ResourceWithThread.__del__ is never called                                                                                                                                                            
      # 4. The non-daemon thread keeps the process alive                                                                                                                                                         
                                                                                                                                                                                                                 
  if __name__ == "__main__":                                                                                                                                                                                     
      main()                                                                                                                                                                                                     
      print("This prints, but process never exits")                                                                                                                                                              

The reference chain that prevents the exit looks something like:

  loguru exception handler → traceback → stack frames → local variables → ResourceWithThread → thread  

Is there a recommended pattern or API I'm missing that would allow me to keep diagnose=True while ensuring traceback references are released after the exception is logged?

Thank you for the wonderful library.

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionFurther information is requested

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions