diff --git a/pygit2/blob.py b/pygit2/blob.py index 1ee6a9eb7..cb496c533 100644 --- a/pygit2/blob.py +++ b/pygit2/blob.py @@ -86,10 +86,20 @@ def readinto(self, b, /): def close(self) -> None: try: - self._ready.wait() - self._writer_closed.wait() - while self._queue is not None and not self._queue.empty(): - self._queue.get() + # The writer thread may be blocked in queue.put() because the + # queue (maxsize=1) still holds a chunk that was never consumed + # (e.g. the reader stopped before reaching EOF). Draining the + # queue must happen *before* (not after) waiting for + # `_writer_closed`, otherwise the writer can never make progress + # to reach its close callback and this would deadlock. + while True: + self._ready.wait() + while self._queue is not None and not self._queue.empty(): + self._queue.get() + if self._writer_closed.is_set(): + # Done + break + self._ready.clear() self._thread.join() except KeyboardInterrupt: pass diff --git a/test/conftest.py b/test/conftest.py index 69d3e6389..08037c56d 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -80,6 +80,12 @@ def testrepo(tmp_path: Path) -> Generator[Repository, None, None]: yield pygit2.Repository(path) +@pytest.fixture +def bigrepo(tmp_path: Path) -> Generator[Repository, None, None]: + with utils.TemporaryRepository('bigrepo.zip', tmp_path) as path: + yield pygit2.Repository(path) + + @pytest.fixture def testrepo_path(tmp_path: Path) -> Generator[tuple[Repository, Path], None, None]: with utils.TemporaryRepository('testrepo.zip', tmp_path) as path: diff --git a/test/data/bigrepo.zip b/test/data/bigrepo.zip new file mode 100644 index 000000000..93b3bcb0f Binary files /dev/null and b/test/data/bigrepo.zip differ diff --git a/test/test_blob.py b/test/test_blob.py index fdc78a2ac..2773a074d 100644 --- a/test/test_blob.py +++ b/test/test_blob.py @@ -306,3 +306,16 @@ def test_blob_write_to_queue_invalid_commit_id_str(testrepo: Repository) -> None flags=BlobFilter.ATTRIBUTES_FROM_COMMIT, commit_id='not-a-valid-oid', # type: ignore[arg-type] ) + + +def test_blob_partial_read(bigrepo: Repository) -> None: + blob_oid = bigrepo.create_blob_fromworkdir('big.txt') + blob = bigrepo[blob_oid] + assert isinstance(blob, pygit2.Blob) + reader = pygit2.BlobIO(blob) + # Read only a few lines then break early + for i, line in enumerate(reader): + if i >= 3: + break + reader.close() + assert not reader.raw._thread.is_alive() # type: ignore[attr-defined]