179 lines
5.8 KiB
Python
179 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""tier_manager.py — Tiered storage management for unbounded memory.
|
|
|
|
Hot → warm → cold based on access patterns.
|
|
Entries NEVER get deleted — they get demoted to colder tiers.
|
|
Cold storage is compressed, not indexed, not searched — but still there.
|
|
Accessed again → promoted back to warm/hot.
|
|
|
|
Run under ~/.venvs/memory/bin/python3.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime, timezone, timedelta
|
|
from typing import Dict, Optional
|
|
|
|
# Tier thresholds (days since last access)
|
|
HOT_THRESHOLD = 7 # accessed within 7 days = hot
|
|
WARM_THRESHOLD = 30 # accessed within 30 days = warm
|
|
# > 30 days = cold
|
|
|
|
# Tier storage characteristics
|
|
TIER_CONFIG = {
|
|
"hot": {
|
|
"indexed": True,
|
|
"cached": True,
|
|
"compressed": False,
|
|
"searched": True,
|
|
"description": "Instant access, indexed, cached",
|
|
},
|
|
"warm": {
|
|
"indexed": True,
|
|
"cached": False,
|
|
"compressed": False,
|
|
"searched": True,
|
|
"description": "Fast access, indexed, not cached",
|
|
},
|
|
"cold": {
|
|
"indexed": False,
|
|
"cached": False,
|
|
"compressed": True,
|
|
"searched": False,
|
|
"description": "Slow access, compressed, not searched",
|
|
},
|
|
}
|
|
|
|
|
|
class TierManager:
|
|
"""Manages tiered storage for entries based on access patterns."""
|
|
|
|
def __init__(self):
|
|
pass
|
|
|
|
def get_tier(self, entry: dict) -> str:
|
|
"""Determine the tier for an entry based on its access patterns.
|
|
|
|
Returns 'hot', 'warm', or 'cold'.
|
|
"""
|
|
wash_count = entry.get("wash_count", 0) or 0
|
|
last_accessed = entry.get("last_accessed")
|
|
rock_status = entry.get("rock_status", "washable")
|
|
|
|
# Rock/locked entries are always hot
|
|
if rock_status == "rock":
|
|
return "hot"
|
|
|
|
# High proven_uses = hot
|
|
if wash_count >= 20:
|
|
return "hot"
|
|
|
|
# Check recency
|
|
if last_accessed:
|
|
try:
|
|
last = datetime.fromisoformat(last_accessed)
|
|
hours_since = (datetime.now(timezone.utc) - last).total_seconds() / 3600
|
|
|
|
if hours_since < HOT_THRESHOLD * 24:
|
|
return "hot"
|
|
elif hours_since < WARM_THRESHOLD * 24:
|
|
return "warm"
|
|
else:
|
|
return "cold"
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
# Default: warm (new entries start here)
|
|
return "warm"
|
|
|
|
def promote(self, entry: dict) -> str:
|
|
"""Promote an entry to a higher tier. Returns new tier."""
|
|
current = self.get_tier(entry)
|
|
if current == "cold":
|
|
return "warm"
|
|
elif current == "warm":
|
|
return "hot"
|
|
return "hot" # already hot
|
|
|
|
def demote(self, entry: dict) -> str:
|
|
"""Demote an entry to a lower tier. Returns new tier."""
|
|
current = self.get_tier(entry)
|
|
rock_status = entry.get("rock_status", "washable")
|
|
|
|
# Never demote rock/locked entries
|
|
if rock_status == "rock":
|
|
return "hot"
|
|
|
|
if current == "hot":
|
|
return "warm"
|
|
elif current == "warm":
|
|
return "cold"
|
|
return "cold" # already cold
|
|
|
|
def demotion_sweep(self, entry: dict) -> str:
|
|
"""Sweep an entry for demotion. Returns the tier it should be in.
|
|
|
|
Same as get_tier but explicitly named for the sweep operation.
|
|
"""
|
|
return self.get_tier(entry)
|
|
|
|
def get_tier_config(self, tier: str) -> dict:
|
|
"""Get the configuration for a tier."""
|
|
return TIER_CONFIG.get(tier, TIER_CONFIG["warm"])
|
|
|
|
def should_search(self, tier: str) -> bool:
|
|
"""Whether entries in this tier should be searched."""
|
|
return TIER_CONFIG.get(tier, {}).get("searched", False)
|
|
|
|
def should_compress(self, tier: str) -> bool:
|
|
"""Whether entries in this tier should be compressed."""
|
|
return TIER_CONFIG.get(tier, {}).get("compressed", False)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# CLI
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
def main():
|
|
import argparse
|
|
parser = argparse.ArgumentParser(description="Tier manager for unbounded memory")
|
|
sub = parser.add_subparsers(dest="cmd")
|
|
|
|
sub.add_parser("config", help="Show tier configuration")
|
|
|
|
p_tier = sub.add_parser("tier", help="Get tier for an entry")
|
|
p_tier.add_argument("--wash-count", type=int, default=0)
|
|
p_tier.add_argument("--last-accessed", default=None)
|
|
p_tier.add_argument("--rock-status", default="washable")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if not args.cmd:
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
tm = TierManager()
|
|
|
|
if args.cmd == "config":
|
|
print("Tier Configuration:")
|
|
for tier, config in TIER_CONFIG.items():
|
|
print(f" {tier}: {config['description']}")
|
|
print(f" indexed={config['indexed']}, cached={config['cached']}, compressed={config['compressed']}, searched={config['searched']}")
|
|
|
|
elif args.cmd == "tier":
|
|
entry = {
|
|
"wash_count": args.wash_count,
|
|
"last_accessed": args.last_accessed,
|
|
"rock_status": args.rock_status,
|
|
}
|
|
tier = tm.get_tier(entry)
|
|
config = tm.get_tier_config(tier)
|
|
print(f"Entry: wash_count={args.wash_count}, rock_status={args.rock_status}")
|
|
print(f"Tier: {tier}")
|
|
print(f"Config: {config['description']}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|