diff --git a/integration/test_backup_v4.py b/integration/test_backup_v4.py index b09f29133..318262f5f 100644 --- a/integration/test_backup_v4.py +++ b/integration/test_backup_v4.py @@ -497,13 +497,13 @@ def test_backup_and_restore_with_collection_and_config_1_24_x( @pytest.mark.parametrize("dynamic_backup_location", [False, True]) -def test_cancel_backup( +def test_cancel_backup_create( client: weaviate.WeaviateClient, dynamic_backup_location: bool, tmp_path: pathlib.Path, request: SubRequest, ) -> None: - """Cancel backup without waiting.""" + """Cancel backup create without waiting.""" backup_id = unique_backup_id(request.node.name) if client._connection._weaviate_version.is_lower_than(1, 24, 25): pytest.skip("Cancel backups is only supported from 1.24.25") @@ -543,6 +543,67 @@ def test_cancel_backup( assert status_resp.status == BackupStatus.CANCELED or status_resp.status == BackupStatus.SUCCESS +@pytest.mark.parametrize("dynamic_backup_location", [False, True]) +def test_cancel_backup_restore( + client: weaviate.WeaviateClient, + dynamic_backup_location: bool, + tmp_path: pathlib.Path, + request: SubRequest, +) -> None: + """Cancel backup restore without waiting.""" + backup_id = unique_backup_id(request.node.name) + if client._connection._weaviate_version.is_lower_than(1, 36, 0): + pytest.skip("Cancel restores is only supported from 1.36.0") + + backup_location: Optional[wvc.backup.BackupLocationType] = None + if dynamic_backup_location: + backup_location = wvc.backup.BackupLocation.FileSystem(path=str(tmp_path)) + + c_name = "Restore" + c = client.collections.create( + name=c_name, properties=[Property(name="name", data_type=DataType.TEXT)] + ) + c.data.insert({"name": "test"}) + + resp = client.backup.create( + backup_id=backup_id, + backend=BACKEND, + backup_location=backup_location, + include_collections=[c_name], + wait_for_completion=True, + ) + assert resp.status == BackupStatus.SUCCESS + + client.collections.delete(c_name) + + resp = client.backup.restore( + backup_id=backup_id, + backend=BACKEND, + backup_location=backup_location, + include_collections=[c_name], + ) + assert resp.status == BackupStatus.STARTED + + assert client.backup.cancel( + backup_id=backup_id, backend=BACKEND, backup_location=backup_location, operation="restore" + ) + + start = time.time() + while time.time() - start < 5: + status_resp = client.backup.get_restore_status( + backup_id=backup_id, backend=BACKEND, backup_location=backup_location + ) + if status_resp.status == BackupStatus.CANCELED: + break + time.sleep(0.1) + + status_resp = client.backup.get_restore_status( + backup_id=backup_id, backend=BACKEND, backup_location=backup_location + ) + # there can be a race between the cancel and the backup completion + assert status_resp.status == BackupStatus.CANCELED or status_resp.status == BackupStatus.SUCCESS + + def test_backup_and_restore_with_roles_and_users( client_factory: ClientFactory, request: SubRequest ) -> None: diff --git a/weaviate/backup/async_.pyi b/weaviate/backup/async_.pyi index 7d7454972..25ab4fe9c 100644 --- a/weaviate/backup/async_.pyi +++ b/weaviate/backup/async_.pyi @@ -54,6 +54,7 @@ class _BackupAsync(_BackupExecutor[ConnectionAsync]): backup_id: str, backend: BackupStorage, backup_location: Optional[BackupLocationType] = None, + operation: Literal["create", "restore"] = "create", ) -> bool: ... async def list_backups( self, backend: BackupStorage, sort_by_starting_time_asc: Optional[bool] = None diff --git a/weaviate/backup/executor.py b/weaviate/backup/executor.py index a14fe1255..fedbac50e 100644 --- a/weaviate/backup/executor.py +++ b/weaviate/backup/executor.py @@ -427,6 +427,7 @@ def cancel( backup_id: str, backend: BackupStorage, backup_location: Optional[BackupLocationType] = None, + operation: Literal["create", "restore"] = "create", ) -> executor.Result[bool]: """Cancels a running backup. @@ -434,6 +435,7 @@ def cancel( backup_id: The identifier name of the backup. NOTE: Case insensitive. backend: The backend storage where to create the backup. backup_location: The dynamic location of a backup. By default None. + operation: The type of the backup operation to cancel, either "create" or "restore". By default "create". Raises: weaviate.exceptions.UnexpectedStatusCodeError: If weaviate reports a none OK status. @@ -445,7 +447,7 @@ def cancel( backup_id=backup_id, backend=backend, ) - path = f"/backups/{backend.value}/{backup_id}" + path = f"/backups/{backend.value}/{backup_id}{'/restore' if operation == 'restore' else ''}" params: Dict[str, str] = {} if backup_location is not None: diff --git a/weaviate/backup/sync.pyi b/weaviate/backup/sync.pyi index bb43de390..26bdffcc6 100644 --- a/weaviate/backup/sync.pyi +++ b/weaviate/backup/sync.pyi @@ -54,6 +54,7 @@ class _Backup(_BackupExecutor[ConnectionSync]): backup_id: str, backend: BackupStorage, backup_location: Optional[BackupLocationType] = None, + operation: Literal["create", "restore"] = "create", ) -> bool: ... def list_backups( self, backend: BackupStorage, sort_by_starting_time_asc: Optional[bool] = None