diff --git a/server.py b/server.py index c6a4cd1..ef227b7 100644 --- a/server.py +++ b/server.py @@ -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'
Cannot discard: item is pinned ({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] + return 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: return f'
Cannot discard: {reason}
'