Skip to content

Commit 266155d

Browse files
committed
Add finaliser to close open stores
1 parent 57f9b98 commit 266155d

File tree

1 file changed

+32
-0
lines changed

1 file changed

+32
-0
lines changed

tsinfer/formats.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import sys
3232
import threading
3333
import warnings
34+
import weakref
3435
from typing import Union # noqa: F401
3536

3637
import attr
@@ -491,6 +492,9 @@ def __init__(
491492
object_codec=self._metadata_codec,
492493
)
493494

495+
# Register a backstop to close the current store if the object leaks
496+
self._set_store_finalizer()
497+
494498
def __enter__(self):
495499
return self
496500

@@ -509,6 +513,8 @@ def _open_readonly(self):
509513
self.data = zarr.open(store=store, mode="r")
510514
self._check_format()
511515
self._mode = self.READ_MODE
516+
# Refresh finalizer to target the current store
517+
self._set_store_finalizer()
512518

513519
def _new_lmdb_store(self, map_size=None):
514520
if os.path.exists(self.path):
@@ -543,6 +549,8 @@ def close(self):
543549
"""
544550
if self._mode != self.READ_MODE:
545551
self.finalise()
552+
# Detach backstop to avoid double-closing on explicit close
553+
self._detach_store_finalizer()
546554
if self.data.store is not None:
547555
self.data.store.close()
548556
self.data = None
@@ -596,6 +604,8 @@ def finalise(self):
596604
self.data.attrs[FINALISED_KEY] = True
597605
if self.path is not None:
598606
store = self.data.store
607+
# The store is about to change; detach any backstop on the old store
608+
self._detach_store_finalizer()
599609
store.close()
600610
logger.debug("Fixing up LMDB file size")
601611
with lmdb.open(self.path, subdir=False, lock=False, writemap=True) as db:
@@ -611,6 +621,28 @@ def finalise(self):
611621
remove_lmdb_lockfile(self.path)
612622
self._open_readonly()
613623

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+
614646
def _check_format(self):
615647
try:
616648
format_name = self.format_name

0 commit comments

Comments
 (0)