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:
|
def _verified_object_size(client: AlistClient, path: str) -> int | None:
|
||||||
return _object_size(client.object_info(path))
|
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(
|
def trash_object(
|
||||||
self,
|
self,
|
||||||
source_id: str,
|
source_id: str,
|
||||||
@@ -508,15 +530,17 @@ class OpenListNativeService:
|
|||||||
# OpenList can finish a provider-side move after the control-plane
|
# OpenList can finish a provider-side move after the control-plane
|
||||||
# request times out. Resolve that ambiguous response from object
|
# request times out. Resolve that ambiguous response from object
|
||||||
# state before deciding whether compensation is required.
|
# 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
|
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:
|
if client.object_info(original) is not None:
|
||||||
raise RuntimeError("OpenList 原生回收站移动后源对象仍然存在")
|
raise RuntimeError("OpenList 原生回收站移动后源对象仍然存在")
|
||||||
if self._verified_object_size(client, target) != expected_size:
|
if self._verified_object_size(client, target) != expected_size:
|
||||||
raise RuntimeError("OpenList 原生回收站对象长度不一致")
|
raise RuntimeError("OpenList 原生回收站对象长度不一致")
|
||||||
if config.encrypted:
|
raise RuntimeError("OpenList 原生回收站移动状态未收敛")
|
||||||
self.sources.rclone.stop(source_id)
|
|
||||||
return target
|
|
||||||
|
|
||||||
def restore_object(
|
def restore_object(
|
||||||
self,
|
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}
|
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():
|
def test_native_trash_restore_conflicts_are_non_destructive_and_purge_is_idempotent():
|
||||||
trash_path = "cloud/library/.imagefind-native-trash/trash-id/opaque.bin"
|
trash_path = "cloud/library/.imagefind-native-trash/trash-id/opaque.bin"
|
||||||
client = _MovingClient({"cloud/library/opaque.bin": 123, trash_path: 123})
|
client = _MovingClient({"cloud/library/opaque.bin": 123, trash_path: 123})
|
||||||
|
|||||||
Reference in New Issue
Block a user