fix: tolerate OpenList move consistency lag

This commit is contained in:
2026-08-13 16:10:53 +08:00
parent 9b8880a3da
commit 38deb8d593
2 changed files with 51 additions and 4 deletions
+28 -4
View File
@@ -484,6 +484,28 @@ class OpenListNativeService:
def _verified_object_size(client: AlistClient, path: str) -> int | None:
return _object_size(client.object_info(path))
@classmethod
def _moved_object_converged(
cls,
client: AlistClient,
original: str,
target: str,
expected_size: int,
*,
attempts: int = 12,
delay_seconds: float = 0.25,
) -> bool:
"""Wait briefly for an eventually-consistent provider move to become visible."""
for attempt in range(attempts):
original_missing = client.object_info(original) is None
target_size = cls._verified_object_size(client, target)
if original_missing and target_size == expected_size:
return True
if attempt + 1 < attempts:
time.sleep(delay_seconds)
return False
def trash_object(
self,
source_id: str,
@@ -508,15 +530,17 @@ class OpenListNativeService:
# OpenList can finish a provider-side move after the control-plane
# request times out. Resolve that ambiguous response from object
# state before deciding whether compensation is required.
if client.object_info(original) is not None or self._verified_object_size(client, target) != expected_size:
if not self._moved_object_converged(client, original, target, expected_size):
raise
if self._moved_object_converged(client, original, target, expected_size):
if config.encrypted:
self.sources.rclone.stop(source_id)
return target
if client.object_info(original) is not None:
raise RuntimeError("OpenList 原生回收站移动后源对象仍然存在")
if self._verified_object_size(client, target) != expected_size:
raise RuntimeError("OpenList 原生回收站对象长度不一致")
if config.encrypted:
self.sources.rclone.stop(source_id)
return target
raise RuntimeError("OpenList 原生回收站移动状态未收敛")
def restore_object(
self,
+23
View File
@@ -198,6 +198,29 @@ def test_native_trash_and_restore_verify_provider_state(ambiguous_timeout: bool)
assert client.files == {"cloud/library/opaque.bin": 123}
def test_native_trash_waits_for_eventually_consistent_provider(monkeypatch):
class EventuallyConsistentClient(_MovingClient):
stale_reads = 4
def object_info(self, path: str):
trash_path = "cloud/library/.imagefind-native-trash/trash-id/opaque.bin"
if self.stale_reads and path in {"cloud/library/opaque.bin", trash_path}:
self.stale_reads -= 1
if path == "cloud/library/opaque.bin":
return {"name": "opaque.bin", "size": 123, "is_dir": False}
return None
return super().object_info(path)
monkeypatch.setattr("imagefind.openlist_native.time.sleep", lambda _seconds: None)
client = EventuallyConsistentClient({"cloud/library/opaque.bin": 123})
service = _native_service(client)
trash_path = service.trash_object("source", "trash-id", "cloud/library/opaque.bin", 123)
assert trash_path == "cloud/library/.imagefind-native-trash/trash-id/opaque.bin"
assert client.files == {trash_path: 123}
def test_native_trash_restore_conflicts_are_non_destructive_and_purge_is_idempotent():
trash_path = "cloud/library/.imagefind-native-trash/trash-id/opaque.bin"
client = _MovingClient({"cloud/library/opaque.bin": 123, trash_path: 123})