From cc9a89d4a72a6d6d2161f5e3de2a005dc15c26ad Mon Sep 17 00:00:00 2001 From: Vladimir Rudnyh Date: Wed, 8 Jul 2026 12:49:56 +0700 Subject: [PATCH] fix: close ODB index after remote operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get_index(odb) opens a sqlite-backed diskcache that push/pull/status and cloud gc never closed, leaking a connection per call until GC (surfaces as "ResourceWarning: unclosed database", e.g. under pytest coverage). close() already existed on the index — just wrap the calls with contextlib.closing. --- dvc/data_cloud.py | 44 ++++++++++++++++++++++++++------------------ dvc/repo/gc.py | 5 +++-- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/dvc/data_cloud.py b/dvc/data_cloud.py index 12d40317a7..9f1a2348e1 100644 --- a/dvc/data_cloud.py +++ b/dvc/data_cloud.py @@ -1,6 +1,7 @@ """Manages dvc remotes that user can use with push/pull/status commands.""" from collections.abc import Iterable +from contextlib import closing from typing import TYPE_CHECKING, Optional from dvc.config import NoRemoteError, RemoteConfigError @@ -210,16 +211,19 @@ def _push( cache = self.repo.cache.legacy else: cache = self.repo.cache.local - with TqdmCallback( - desc=f"Pushing to {odb.fs.unstrip_protocol(odb.path)}", - unit="file", - ) as cb: + with ( + closing(get_index(odb)) as index, + TqdmCallback( + desc=f"Pushing to {odb.fs.unstrip_protocol(odb.path)}", + unit="file", + ) as cb, + ): return self.transfer( cache, odb, objs, jobs=jobs, - dest_index=get_index(odb), + dest_index=index, cache_odb=cache, validate_status=self._log_missing, callback=cb, @@ -271,16 +275,19 @@ def _pull( cache = self.repo.cache.legacy else: cache = self.repo.cache.local - with TqdmCallback( - desc=f"Fetching from {odb.fs.unstrip_protocol(odb.path)}", - unit="file", - ) as cb: + with ( + closing(get_index(odb)) as index, + TqdmCallback( + desc=f"Fetching from {odb.fs.unstrip_protocol(odb.path)}", + unit="file", + ) as cb, + ): return self.transfer( odb, cache, objs, jobs=jobs, - src_index=get_index(odb), + src_index=index, cache_odb=cache, verify=odb.verify, validate_status=self._log_missing, @@ -340,14 +347,15 @@ def _status( cache = self.repo.cache.legacy else: cache = self.repo.cache.local - return compare_status( - cache, - odb, - objs, - jobs=jobs, - dest_index=get_index(odb), - cache_odb=cache, - ) + with closing(get_index(odb)) as index: + return compare_status( + cache, + odb, + objs, + jobs=jobs, + dest_index=index, + cache_odb=cache, + ) def get_url_for(self, remote, checksum): odb = self.get_remote_odb(remote) diff --git a/dvc/repo/gc.py b/dvc/repo/gc.py index 95b4b3b760..64f8d1afe3 100644 --- a/dvc/repo/gc.py +++ b/dvc/repo/gc.py @@ -88,7 +88,7 @@ def gc( # noqa: C901, PLR0912, PLR0913 not_in_remote=not_in_remote, ) - from contextlib import ExitStack + from contextlib import ExitStack, closing from dvc.repo import Repo from dvc_data.hashfile.db import get_index @@ -148,7 +148,8 @@ def gc( # noqa: C901, PLR0912, PLR0913 assert remote_odb is not None num_removed = ogc(remote_odb, obj_ids, jobs=jobs, dry=dry) if num_removed: - get_index(remote_odb).clear() + with closing(get_index(remote_odb)) as index: + index.clear() logger.info("Removed %d objects from remote.", num_removed) else: logger.info("No unused cache to remove from remote.")