fix: tolerate OpenList move consistency lag
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user