@@ -29,21 +29,52 @@ def trace(self, message, *args, **kws):
2929
3030class WindowsSafeRotatingFileHandler (RotatingFileHandler ):
3131 """A RotatingFileHandler that handles Windows file locking gracefully."""
32+
33+ def shouldRollover (self , record ):
34+ retry_after = getattr (self , "_rollover_retry_after" , 0 )
35+ if retry_after and time .monotonic () < retry_after :
36+ return False
37+ return super ().shouldRollover (record )
38+
39+ def _rollover_to_timestamped_file (self ):
40+ if getattr (self , "stream" , None ) is not None :
41+ try :
42+ self .stream .close ()
43+ finally :
44+ self .stream = None
45+
46+ if not os .path .exists (self .baseFilename ):
47+ self .stream = self ._open ()
48+ return None
49+
50+ timestamp = int (time .time () * 1000 )
51+ fallback_path = f"{ self .baseFilename } .{ timestamp } .rollover"
52+ suffix = 1
53+ while os .path .exists (fallback_path ):
54+ fallback_path = f"{ self .baseFilename } .{ timestamp } .{ suffix } .rollover"
55+ suffix += 1
56+
57+ self .rotate (self .baseFilename , fallback_path )
58+ if not self .delay :
59+ self .stream = self ._open ()
60+ return fallback_path
3261
3362 def doRollover (self ):
3463 """Perform rollover with retry logic for Windows file locking issues."""
3564 if sys .platform != "win32" :
3665 return super ().doRollover ()
3766
3867 max_retries = 3
68+ _last_err = None
3969 for attempt in range (max_retries ):
4070 try :
4171 super ().doRollover ()
72+ self ._rollover_retry_after = 0
4273 return
43- except PermissionError :
74+ except PermissionError as _perm_err :
75+ _last_err = _perm_err
4476 if attempt < max_retries - 1 :
45- import time
46- time .sleep (0.1 )
77+ time .sleep (0.1 * (attempt + 1 ))
4778 except Exception as _other_err :
4879 _last_err = _other_err
4980 # Non-permission errors on Windows usually mean a stuck
@@ -63,6 +94,21 @@ def doRollover(self):
6394 )
6495 except Exception :
6596 pass
97+ try :
98+ fallback_path = self ._rollover_to_timestamped_file ()
99+ if fallback_path :
100+ self ._rollover_retry_after = 0
101+ try :
102+ sys .stderr .write (
103+ f"[WindowsSafeRotatingFileHandler] fallback rollover "
104+ f"used { fallback_path !r} for { self .baseFilename !r} \n "
105+ )
106+ except Exception :
107+ pass
108+ return
109+ except Exception as _fallback_err :
110+ _last_err = _fallback_err
111+
66112 # All retries failed. The stock RotatingFileHandler.doRollover
67113 # closes `self.stream` BEFORE attempting the rename, so a failed
68114 # rotation leaves the handler with a closed FD. Subsequent
@@ -72,7 +118,7 @@ def doRollover(self):
72118 # went dark while the app kept running for several more minutes.
73119 # Make the failure visible AND guarantee we keep a live stream,
74120 # even if that means appending to the already-oversized file.
75- _last_err = locals (). get ( "_last_err" , None )
121+ self . _rollover_retry_after = time . monotonic () + 30.0
76122 try :
77123 sys .stderr .write (
78124 f"[WindowsSafeRotatingFileHandler] rollover FAILED for "
0 commit comments