Use local pinned metadata for deletion checks instead of L2 API
- Add is_pinned(), pin(), _load_meta(), _save_meta() to L1CacheManager - Update can_delete() and can_discard_activity() to check local pinned status - Update run deletion endpoints (API and UI) to check pinned metadata - Remove L2 shared check fallback from run deletion - Fix L2SharedChecker to return True on error (safer - prevents accidental deletion) - Update tests for new pinned behavior When items are published to L2, the publish flow marks them as pinned locally. This ensures items remain non-deletable even if L2 is unreachable, and both outputs AND inputs of published runs are protected. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -86,19 +86,20 @@ class L2SharedChecker:
|
||||
if content_hash in self._cache:
|
||||
is_shared, cached_at = self._cache[content_hash]
|
||||
if now - cached_at < self.cache_ttl:
|
||||
logger.debug(f"L2 check (cached): {content_hash[:16]}... = {is_shared}")
|
||||
return is_shared
|
||||
|
||||
# Query L2
|
||||
try:
|
||||
resp = requests.get(
|
||||
f"{self.l2_server}/registry/by-hash/{content_hash}",
|
||||
timeout=5
|
||||
)
|
||||
url = f"{self.l2_server}/registry/by-hash/{content_hash}"
|
||||
logger.info(f"L2 check: GET {url}")
|
||||
resp = requests.get(url, timeout=5)
|
||||
logger.info(f"L2 check response: {resp.status_code}")
|
||||
is_shared = resp.status_code == 200
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to check L2 for {content_hash}: {e}")
|
||||
# On error, assume not shared (safer for deletion)
|
||||
is_shared = False
|
||||
# On error, assume IS shared (safer - prevents accidental deletion)
|
||||
is_shared = True
|
||||
|
||||
self._cache[content_hash] = (is_shared, now)
|
||||
return is_shared
|
||||
@@ -184,6 +185,39 @@ class L1CacheManager:
|
||||
"""Check if a content_hash is shared via L2."""
|
||||
return self.l2_checker.is_shared(content_hash)
|
||||
|
||||
def _load_meta(self, content_hash: str) -> dict:
|
||||
"""Load metadata for a cached file."""
|
||||
meta_path = self.cache_dir / f"{content_hash}.meta.json"
|
||||
if meta_path.exists():
|
||||
with open(meta_path) as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
def is_pinned(self, content_hash: str) -> tuple[bool, str]:
|
||||
"""
|
||||
Check if a content_hash is pinned (non-deletable).
|
||||
|
||||
Returns:
|
||||
(is_pinned, reason) tuple
|
||||
"""
|
||||
meta = self._load_meta(content_hash)
|
||||
if meta.get("pinned"):
|
||||
return True, meta.get("pin_reason", "published")
|
||||
return False, ""
|
||||
|
||||
def _save_meta(self, content_hash: str, **updates) -> dict:
|
||||
"""Save/update metadata for a cached file."""
|
||||
meta = self._load_meta(content_hash)
|
||||
meta.update(updates)
|
||||
meta_path = self.cache_dir / f"{content_hash}.meta.json"
|
||||
with open(meta_path, "w") as f:
|
||||
json.dump(meta, f, indent=2)
|
||||
return meta
|
||||
|
||||
def pin(self, content_hash: str, reason: str = "published") -> None:
|
||||
"""Mark an item as pinned (non-deletable)."""
|
||||
self._save_meta(content_hash, pinned=True, pin_reason=reason)
|
||||
|
||||
# ============ File Storage ============
|
||||
|
||||
def put(
|
||||
@@ -396,9 +430,10 @@ class L1CacheManager:
|
||||
Returns:
|
||||
(can_delete, reason) tuple
|
||||
"""
|
||||
# Check if shared via L2
|
||||
if self.l2_checker.is_shared(content_hash):
|
||||
return False, "Item is published to L2"
|
||||
# Check if pinned (published or input to published)
|
||||
pinned, reason = self.is_pinned(content_hash)
|
||||
if pinned:
|
||||
return False, f"Item is pinned ({reason})"
|
||||
|
||||
# Find node_id for this content
|
||||
node_id = self._content_index.get(content_hash, content_hash)
|
||||
@@ -423,11 +458,13 @@ class L1CacheManager:
|
||||
if not activity:
|
||||
return False, "Activity not found"
|
||||
|
||||
# Check if any item is shared
|
||||
# Check if any item is pinned
|
||||
for node_id in activity.all_node_ids:
|
||||
entry = self.cache.get_entry(node_id)
|
||||
if entry and self.l2_checker.is_shared(entry.content_hash):
|
||||
return False, f"Item {node_id} is published to L2"
|
||||
if entry:
|
||||
pinned, reason = self.is_pinned(entry.content_hash)
|
||||
if pinned:
|
||||
return False, f"Item {node_id} is pinned ({reason})"
|
||||
|
||||
return True, "OK"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user