fix: quarantine failed product cleanups in failed_cleanup state
This commit is contained in:
@@ -126,7 +126,9 @@ class CleanupService:
|
||||
if product_dir.exists():
|
||||
shutil.rmtree(product_dir)
|
||||
except Exception:
|
||||
self.product_store.release_product_purge(product.product_id)
|
||||
# rmtree may have removed only part of the archive. Quarantine
|
||||
# the record for manual inspection instead of retrying it.
|
||||
self.product_store.fail_product_purge(product.product_id)
|
||||
raise
|
||||
self.product_store.finalize_product_purge(product.product_id, now=now_utc)
|
||||
report.deleted_product_ids.append(product.product_id)
|
||||
|
||||
@@ -21,7 +21,7 @@ from .schemas import ProductVersionRecord
|
||||
|
||||
|
||||
class ProductPendingCleanupError(ValueError):
|
||||
"""Raised when a soft-deleted product is changed before restoration."""
|
||||
"""Raised when a pending or failed-cleanup product is changed before recovery."""
|
||||
|
||||
|
||||
class ProductArchiveService:
|
||||
@@ -83,7 +83,7 @@ class ProductArchiveService:
|
||||
if not isinstance(document, dict):
|
||||
raise ValueError("document must be a JSON object")
|
||||
product = self.store.get_product(product_id)
|
||||
if product.status == "pending_cleanup":
|
||||
if product.status in ("pending_cleanup", "failed_cleanup"):
|
||||
raise ProductPendingCleanupError("product is pending cleanup")
|
||||
if product.status != "active":
|
||||
raise ValueError("product is not active")
|
||||
|
||||
@@ -318,7 +318,9 @@ class ProductArchiveStore:
|
||||
raise ValueError(f"unknown version_id for product: {version_id}")
|
||||
|
||||
def mark_pending_cleanup(self, product_id: str, now: datetime) -> ProductRecord:
|
||||
self.get_product(product_id)
|
||||
product = self.get_product(product_id)
|
||||
if product.status not in ("active", "failed_cleanup"):
|
||||
raise ValueError("product cannot be marked pending cleanup")
|
||||
purge_after = now + timedelta(days=30)
|
||||
self._execute(
|
||||
"UPDATE products SET status = ?, purge_after = ?, updated_at = ? WHERE product_id = ?",
|
||||
@@ -355,8 +357,8 @@ class ProductArchiveStore:
|
||||
)
|
||||
conn.execute(
|
||||
"""UPDATE cleanup_records SET status = ?, completed_at = ?
|
||||
WHERE product_id = ? AND status = ?""",
|
||||
("restored", restored_at.isoformat(), product_id, "pending_cleanup"),
|
||||
WHERE product_id = ? AND status IN ('pending_cleanup', 'failed_cleanup')""",
|
||||
("restored", restored_at.isoformat(), product_id),
|
||||
)
|
||||
return self.get_product(product_id)
|
||||
|
||||
@@ -405,13 +407,29 @@ class ProductArchiveStore:
|
||||
return None
|
||||
return self._product_from_row(row)
|
||||
|
||||
def release_product_purge(self, product_id: str) -> None:
|
||||
"""Return a failed physical deletion claim to its pending state."""
|
||||
self._execute(
|
||||
"""UPDATE cleanup_records SET status = 'pending_cleanup'
|
||||
WHERE product_id = ? AND status = 'purging'""",
|
||||
(product_id,),
|
||||
)
|
||||
def fail_product_purge(self, product_id: str) -> ProductRecord:
|
||||
"""Quarantine a purge whose physical deletion failed mid-way."""
|
||||
with self._lock, self._connect() as conn:
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
claim = conn.execute(
|
||||
"""SELECT 1 FROM cleanup_records
|
||||
WHERE product_id = ? AND status = 'purging' LIMIT 1""",
|
||||
(product_id,),
|
||||
).fetchone()
|
||||
if claim is None:
|
||||
raise ValueError("product purge is not claimed")
|
||||
updated = conn.execute(
|
||||
"""UPDATE cleanup_records SET status = 'failed_cleanup'
|
||||
WHERE product_id = ? AND status = 'purging'""",
|
||||
(product_id,),
|
||||
)
|
||||
if updated.rowcount != 1:
|
||||
raise ValueError("product purge is not claimed")
|
||||
conn.execute(
|
||||
"UPDATE products SET status = 'failed_cleanup', updated_at = ? WHERE product_id = ?",
|
||||
(_now().isoformat(), product_id),
|
||||
)
|
||||
return self.get_product(product_id)
|
||||
|
||||
def finalize_product_purge(self, product_id: str, now: datetime) -> ProductRecord:
|
||||
"""Atomically mark a claimed product purged after file removal."""
|
||||
|
||||
@@ -43,7 +43,7 @@ class ProductRecord(BaseModel):
|
||||
name: str
|
||||
sku: str = ""
|
||||
specification: str = ""
|
||||
status: Literal["active", "pending_cleanup", "purged"] = "active"
|
||||
status: Literal["active", "pending_cleanup", "failed_cleanup", "purged"] = "active"
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
purge_after: datetime | None = None
|
||||
|
||||
Reference in New Issue
Block a user