31
31
import sys
32
32
import threading
33
33
import warnings
34
+ import weakref
34
35
from typing import Union # noqa: F401
35
36
36
37
import attr
@@ -491,6 +492,9 @@ def __init__(
491
492
object_codec = self ._metadata_codec ,
492
493
)
493
494
495
+ # Register a backstop to close the current store if the object leaks
496
+ self ._set_store_finalizer ()
497
+
494
498
def __enter__ (self ):
495
499
return self
496
500
@@ -509,6 +513,8 @@ def _open_readonly(self):
509
513
self .data = zarr .open (store = store , mode = "r" )
510
514
self ._check_format ()
511
515
self ._mode = self .READ_MODE
516
+ # Refresh finalizer to target the current store
517
+ self ._set_store_finalizer ()
512
518
513
519
def _new_lmdb_store (self , map_size = None ):
514
520
if os .path .exists (self .path ):
@@ -543,6 +549,8 @@ def close(self):
543
549
"""
544
550
if self ._mode != self .READ_MODE :
545
551
self .finalise ()
552
+ # Detach backstop to avoid double-closing on explicit close
553
+ self ._detach_store_finalizer ()
546
554
if self .data .store is not None :
547
555
self .data .store .close ()
548
556
self .data = None
@@ -596,6 +604,8 @@ def finalise(self):
596
604
self .data .attrs [FINALISED_KEY ] = True
597
605
if self .path is not None :
598
606
store = self .data .store
607
+ # The store is about to change; detach any backstop on the old store
608
+ self ._detach_store_finalizer ()
599
609
store .close ()
600
610
logger .debug ("Fixing up LMDB file size" )
601
611
with lmdb .open (self .path , subdir = False , lock = False , writemap = True ) as db :
@@ -611,6 +621,28 @@ def finalise(self):
611
621
remove_lmdb_lockfile (self .path )
612
622
self ._open_readonly ()
613
623
624
+ # The tsinfer test suite creates many DataContainer objects,
625
+ # lots of which are never closed explicitly. On Windows these
626
+ # files can't be deleted until the file handle is closed, so
627
+ # CI fails with "no disk space left on device" errors. To avoid
628
+ # this, we use a finalizer to close the underlying store if
629
+ # the object is garbage collected without being closed.
630
+ def _detach_store_finalizer (self ):
631
+ fin = getattr (self , "_gc_close" , None )
632
+ if fin is not None and getattr (fin , "alive" , False ):
633
+ fin .detach ()
634
+
635
+ def _set_store_finalizer (self ):
636
+ # Detach any previous finalizer and attach to current store
637
+ self ._detach_store_finalizer ()
638
+ store = getattr (self , "data" , None )
639
+ if store is not None :
640
+ store = getattr (self .data , "store" , None )
641
+ if store is not None and hasattr (store , "close" ):
642
+ self ._gc_close = weakref .finalize (self , store .close )
643
+ else :
644
+ self ._gc_close = None
645
+
614
646
def _check_format (self ):
615
647
try :
616
648
format_name = self .format_name
0 commit comments