From e00ea442a02eb9056befc584d44abdc077079f96 Mon Sep 17 00:00:00 2001 From: Zhe Chen Date: Sun, 18 Oct 2026 02:35:25 +0800 Subject: [PATCH 1/2] Fix #2202: Release the thread lock when IPC acquisition fails --- libcloud/storage/drivers/local.py | 16 ++++++++++------ libcloud/test/storage/test_local.py | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/libcloud/storage/drivers/local.py b/libcloud/storage/drivers/local.py index f47d7c0649..51f4887e6b 100644 --- a/libcloud/storage/drivers/local.py +++ b/libcloud/storage/drivers/local.py @@ -102,13 +102,17 @@ def __enter__(self): "in %s seconds" % (self.path, lock_acquire_timeout) ) - success = self.ipc_lock.acquire(blocking=True, timeout=lock_acquire_timeout) + try: + success = self.ipc_lock.acquire(blocking=True, timeout=lock_acquire_timeout) - if not success: - raise LibcloudError( - "Failed to acquire IPC lock (%s) for path %s " - "in %s seconds" % (self.ipc_lock_path, self.path, lock_acquire_timeout) - ) + if not success: + raise LibcloudError( + "Failed to acquire IPC lock (%s) for path %s " + "in %s seconds" % (self.ipc_lock_path, self.path, lock_acquire_timeout) + ) + except BaseException: + self.thread_lock.release() + raise def __exit__(self, type, value, traceback): if self.thread_lock.locked(): diff --git a/libcloud/test/storage/test_local.py b/libcloud/test/storage/test_local.py index 7d7f271405..edfa6e84ee 100644 --- a/libcloud/test/storage/test_local.py +++ b/libcloud/test/storage/test_local.py @@ -22,6 +22,7 @@ import tempfile import unittest import multiprocessing +from unittest.mock import patch from libcloud.utils.files import exhaust_iterator from libcloud.common.types import LibcloudError @@ -139,6 +140,22 @@ def test_lock_local_storage(self): self.assertEqual(bool(success_1.value), True, "Check didn't pass") self.assertEqual(bool(success_2.value), True, "Second check didn't pass") + def test_lock_local_storage_releases_thread_lock_on_ipc_failure(self): + for error in (None, OSError("lock unavailable"), KeyboardInterrupt()): + with self.subTest(error=error): + lock = LockLocalStorage(os.path.join(self.key, "lock-retry")) + first_result = False if error is None else error + with patch.object(lock.ipc_lock, "acquire", side_effect=[first_result, True]): + expected_error = LibcloudError if error is None else type(error) + with self.assertRaises(expected_error) as exc: + lock.__enter__() + if error is not None: + self.assertIs(exc.exception, error) + self.assertFalse(lock.thread_lock.locked()) + with lock: + self.assertTrue(lock.thread_lock.locked()) + self.assertFalse(lock.thread_lock.locked()) + def test_list_containers_empty(self): containers = self.driver.list_containers() self.assertEqual(len(containers), 0) From 95d3a08354fe8294ae3debcd983e7debd3661b53 Mon Sep 17 00:00:00 2001 From: Zhe Chen <112833940+zhechen-cs@users.noreply.github.com> Date: Sat, 7 Nov 2026 00:03:19 +0800 Subject: [PATCH 2/2] Add changelog entry for #2203 --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d226c235ae..873ce1104d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -89,6 +89,11 @@ Compute Storage ~~~~~~~ +- [Local Storage] Release the thread lock if IPC lock acquisition fails, + allowing the same lock object to be reused after a timeout or error. + (#2203) + [Zhe Chen - @zhechen-cs] + - [Storage] Prevent path traversal when downloading objects to a directory by using only the object name's basename as the local filename. (#2206)