Fix deletion rules: runs deletable, cache items protected

- Run deletion: Handle legacy runs without activity records by
  checking L2 shared status directly (instead of failing)
- Cache deletion: Check Redis runs in addition to activity store
  to prevent deleting inputs/outputs that belong to runs
- Add find_runs_using_content() helper to check if content_hash
  is used as input or output of any run

This fixes the inverted deletion logic where runs couldn't be
deleted but their cache items could.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gilesb
2026-01-08 01:29:03 +00:00
parent f8ec42b445
commit a97c6309d5

View File

@@ -74,6 +74,21 @@ def list_all_runs() -> list["RunStatus"]:
runs.append(RunStatus.model_validate_json(data))
return sorted(runs, key=lambda r: r.created_at, reverse=True)
def find_runs_using_content(content_hash: str) -> list[tuple["RunStatus", str]]:
"""Find all runs that use a content_hash as input or output.
Returns list of (run, role) tuples where role is 'input' or 'output'.
"""
results = []
for run in list_all_runs():
if run.inputs and content_hash in run.inputs:
results.append((run, "input"))
if run.output_hash == content_hash:
results.append((run, "output"))
return results
app = FastAPI(
title="Art DAG L1 Server",
description="Distributed rendering server for Art DAG",
@@ -369,15 +384,28 @@ async def discard_run(run_id: str, username: str = Depends(get_required_user)):
if run.username not in (username, actor_id):
raise HTTPException(403, "Access denied")
# Check if run can be discarded
can_discard, reason = cache_manager.can_discard_activity(run_id)
if not can_discard:
raise HTTPException(400, f"Cannot discard run: {reason}")
# Check if activity exists for this run
activity = cache_manager.get_activity(run_id)
# Discard the activity (cleans up cache entries)
success, msg = cache_manager.discard_activity(run_id)
if not success:
raise HTTPException(500, f"Failed to discard: {msg}")
if activity:
# Use activity manager deletion rules
can_discard, reason = cache_manager.can_discard_activity(run_id)
if not can_discard:
raise HTTPException(400, f"Cannot discard run: {reason}")
# Discard the activity (cleans up cache entries)
success, msg = cache_manager.discard_activity(run_id)
if not success:
raise HTTPException(500, f"Failed to discard: {msg}")
else:
# Legacy run without activity record - check L2 shared status manually
items_to_check = list(run.inputs or [])
if run.output_hash:
items_to_check.append(run.output_hash)
for content_hash in items_to_check:
if cache_manager.l2_checker.is_shared(content_hash):
raise HTTPException(400, f"Cannot discard run: item {content_hash[:16]}... is published to L2")
# Remove from Redis
redis_client.delete(f"{RUNS_KEY_PREFIX}{run_id}")
@@ -1500,7 +1528,13 @@ async def discard_cache(content_hash: str, username: str = Depends(get_required_
pin_reason = meta.get("pin_reason", "unknown")
raise HTTPException(400, f"Cannot discard pinned item (reason: {pin_reason})")
# Check deletion rules via cache_manager
# Check if used by any run (Redis runs, not just activity store)
runs_using = find_runs_using_content(content_hash)
if runs_using:
run, role = runs_using[0]
raise HTTPException(400, f"Cannot discard: item is {role} of run {run.run_id}")
# Check deletion rules via cache_manager (L2 shared status, activity store)
can_delete, reason = cache_manager.can_delete(content_hash)
if not can_delete:
raise HTTPException(400, f"Cannot discard: {reason}")
@@ -1546,7 +1580,13 @@ async def ui_discard_cache(content_hash: str, request: Request):
pin_reason = meta.get("pin_reason", "unknown")
return f'<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Cannot discard: item is pinned ({pin_reason})</div>'
# Check deletion rules via cache_manager
# Check if used by any run (Redis runs, not just activity store)
runs_using = find_runs_using_content(content_hash)
if runs_using:
run, role = runs_using[0]
return f'<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Cannot discard: item is {role} of run {run.run_id}</div>'
# Check deletion rules via cache_manager (L2 shared status, activity store)
can_delete, reason = cache_manager.can_delete(content_hash)
if not can_delete:
return f'<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Cannot discard: {reason}</div>'