服务器缓存按期限清理,审计快照默认不再落地

排盘缓存、官方快照、负缓存、网关任务和分盘段过期后会删掉。同一张盘的数字写法合成一把缓存键。失败快照只用短期限。自测留存、异步任务、增长合同、安全缓存和隐私标记;一万个文件的清理慢于 2 秒,命中率提升不到 2.5 倍,都按实测写入记录。

Co-Authored-By: Grok (agent loop)
This commit is contained in:
jesse-ux
2026-10-14 04:53:30 +08:00
co-authored by Grok (agent loop)
parent 66cd60081f
commit 81e2e1cfed
14 changed files with 1677 additions and 65 deletions
+609
View File
@@ -0,0 +1,609 @@
"""Retention rules for scratch/local caches.
Counts only. Never prints file names or cache payloads.
"""
from __future__ import annotations
import json
import math
import os
import re
import secrets
import threading
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Callable
ROOT = Path(__file__).resolve().parents[1]
UNREADABLE_KEEP_SECONDS = 24 * 60 * 60
_HEX64_JSON = re.compile(r"^[0-9a-f]{64}\.json$")
_ARTIFACT_JSON = re.compile(r"^[A-Za-z0-9_]+-[0-9a-f]{12}-[0-9a-f]{12}\.json$")
_GATEWAY_JOB_JSON = re.compile(r"^vgw_[0-9a-f]{20}\.json$")
_PRUNE_THREAD_NAME = "jyotish-scratch-prune"
# Directory names under scratch/local that this module already knows how to prune.
# Subdirectories of vedastro_adapter are listed so a future writer is not a new top-level dir.
REGISTERED_DIR_NAMES = frozenset(
{
"api_chart_cache",
"high_rigor_jobs",
"api_chart_response_jobs",
"async_jobs.sqlite3",
"vedastro_adapter",
"vedastro_snapshot_cache",
"vedastro_snapshot_negative_cache",
"vedastro_gateway_jobs",
"varga_segments",
}
)
# Offline tools or caches that already prune themselves. Not a new retention rule.
OFFLINE_SCRATCH_NAMES = frozenset(
{
"consult_full_data_cache",
"ocr_extract",
"three_engine_parity",
"public_release_deny_patterns.txt",
"pdf_review_123456",
}
)
REGISTRY: tuple[dict[str, Any], ...] = (
{
"name": "api_chart_cache",
"holds_birth_data": True,
"deletable": "created_at plus chart TTL, or unreadable json older than 24h",
},
{
"name": "high_rigor_jobs",
"holds_birth_data": True,
"deletable": "expires_at_unix has passed; bad json is kept",
},
{
"name": "api_chart_response_jobs",
"holds_birth_data": True,
"deletable": "expires_at_unix has passed; bad json is kept",
},
{
"name": "async_jobs.sqlite3",
"holds_birth_data": True,
"deletable": "sqlite rows whose expires_at has passed",
},
{
"name": "vedastro_adapter",
"holds_birth_data": True,
"deletable": "audit artifacts, response_cache, and official_full_snapshot_cache",
},
{
"name": "vedastro_snapshot_cache",
"holds_birth_data": True,
"deletable": "reference_date before today UTC minus MAX_STALE_DAYS minus 1",
},
{
"name": "vedastro_snapshot_negative_cache",
"holds_birth_data": True,
"deletable": "reference_date before today UTC",
},
{
"name": "vedastro_gateway_jobs",
"holds_birth_data": True,
"deletable": "completed or failed jobs older than the keep window, plus their raw archive",
},
{
"name": "varga_segments",
"holds_birth_data": True,
"deletable": "mtime older than JYOTISH_VARGA_SEGMENTS_KEEP_DAYS",
},
)
def write_private_json(path: Path, payload: Any) -> None:
"""Atomic UTF-8 JSON, mode 0600. Compact, stable key order."""
path.parent.mkdir(parents=True, exist_ok=True)
temp = path.with_name(f"{path.name}.{secrets.token_hex(8)}.tmp")
temp.write_text(
json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")),
encoding="utf-8",
)
try:
os.chmod(temp, 0o600)
except OSError:
pass
os.replace(temp, path)
try:
os.chmod(path, 0o600)
except OSError:
pass
def _finite_float(value: Any) -> float | None:
if isinstance(value, bool) or value is None:
return None
if isinstance(value, (int, float)):
number = float(value)
elif isinstance(value, str):
text = value.strip()
if not text:
return None
try:
number = float(text)
except ValueError:
return None
else:
return None
if not math.isfinite(number):
return None
return number
def _exact_int(value: Any) -> int | None:
if isinstance(value, bool) or value is None:
return None
if isinstance(value, int):
return value
if isinstance(value, float):
if not math.isfinite(value) or not value.is_integer():
return None
return int(value)
if isinstance(value, str) and re.fullmatch(r"[+-]?\d+", value.strip()):
return int(value.strip())
return None
def normalize_chart_cache_birth(body: dict | None) -> dict[str, Any]:
"""Cache-key birth fields. Unparseable values stay as they arrived."""
payload = body if isinstance(body, dict) else {}
normalized: dict[str, Any] = {}
for key in ("year", "month", "day"):
parsed = _exact_int(payload.get(key))
normalized[key] = payload.get(key) if parsed is None else parsed
for key in ("hour", "minute", "lat", "lon", "tz"):
raw = payload.get(key)
parsed = _finite_float(raw)
normalized[key] = raw if parsed is None else parsed
second_raw = payload.get("second", 0)
second = _finite_float(second_raw)
normalized["second"] = second_raw if second is None else second
return normalized
def normalize_chart_cache_day(value: Any) -> Any:
if value is None or value == "":
return value
return str(value)[:10]
def _blank() -> dict[str, int]:
return {"scanned": 0, "removed": 0, "errors": 0}
def _older_than(path: Path, moment: float, seconds: float) -> bool:
try:
return moment - path.stat().st_mtime > seconds
except OSError:
return False
def _unlink(path: Path, stats: dict[str, int]) -> None:
try:
path.unlink()
stats["removed"] += 1
except OSError:
stats["errors"] += 1
def _repo_root() -> Path:
return ROOT
def _api_module(api_module=None):
if api_module is not None:
return api_module
import jyotish_api_server as api
return api
def prune_async_job_records(now: float | None = None, api_module=None) -> dict[str, Any]:
"""Same file and sqlite rules as the old startup cleanup. Bad job JSON is kept."""
api = _api_module(api_module)
moment = time.time() if now is None else float(now)
scanned = 0
removed = 0
errors = 0
directories: dict[str, dict[str, int]] = {}
if api._async_job_backend() == "sqlite":
try:
with api._sqlite_job_connection() as connection:
scanned = int(connection.execute("SELECT COUNT(*) FROM async_jobs").fetchone()[0])
deleted = connection.execute(
"DELETE FROM async_jobs WHERE expires_at IS NOT NULL AND expires_at <= ?",
(moment,),
).rowcount
removed = int(deleted or 0)
except OSError:
errors += 1
directories["high_rigor_jobs"] = _blank()
directories["api_chart_response_jobs"] = _blank()
directories["async_jobs_sqlite"] = {"scanned": scanned, "removed": removed, "errors": errors}
return {
"scope": "async_job_cleanup",
"scanned": scanned,
"removed": removed,
"errors": errors,
"directories": directories,
}
for scope, label in (
(api._HIGH_RIGOR_JOB_SCOPE, "high_rigor_jobs"),
(api._API_CHART_CACHE_SCOPE, "api_chart_response_jobs"),
):
stats = _blank()
try:
directory = api._async_job_dir(scope)
except OSError:
stats["errors"] += 1
directories[label] = stats
errors += stats["errors"]
continue
if directory.is_dir():
for path in directory.glob("*.json"):
stats["scanned"] += 1
try:
record = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError:
continue
except OSError:
stats["errors"] += 1
continue
expires_at = record.get("expires_at_unix") if isinstance(record, dict) else None
if isinstance(expires_at, (int, float)) and not isinstance(expires_at, bool) and moment >= float(expires_at):
_unlink(path, stats)
directories[label] = stats
scanned += stats["scanned"]
removed += stats["removed"]
errors += stats["errors"]
return {
"scope": "async_job_cleanup",
"scanned": scanned,
"removed": removed,
"errors": errors,
"directories": directories,
}
def _prune_chart_cache(moment: float, api_module=None) -> dict[str, int]:
api = _api_module(api_module)
stats = _blank()
try:
directory = api._api_chart_cache_dir()
ttl = api._api_chart_cache_ttl_seconds()
except OSError:
stats["errors"] += 1
return stats
if not directory.is_dir():
return stats
for path in directory.glob("*.json"):
if not _HEX64_JSON.match(path.name):
continue
stats["scanned"] += 1
try:
record = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError:
if _older_than(path, moment, UNREADABLE_KEEP_SECONDS):
_unlink(path, stats)
continue
except OSError:
stats["errors"] += 1
continue
created_at = record.get("created_at") if isinstance(record, dict) else None
if not isinstance(created_at, (int, float)) or isinstance(created_at, bool):
if _older_than(path, moment, UNREADABLE_KEEP_SECONDS):
_unlink(path, stats)
continue
if moment - float(created_at) > ttl:
_unlink(path, stats)
return stats
def _adapter():
import vedastro_service_adapter as adapter
return adapter
def _prune_created_at_dir(directory: Path, moment: float, ttl_for_record: Callable[[dict], float]) -> dict[str, int]:
stats = _blank()
if not directory.is_dir():
return stats
for path in directory.glob("*.json"):
if not _HEX64_JSON.match(path.name):
continue
stats["scanned"] += 1
try:
record = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError:
if _older_than(path, moment, UNREADABLE_KEEP_SECONDS):
_unlink(path, stats)
continue
except OSError:
stats["errors"] += 1
continue
created_at = record.get("created_at") if isinstance(record, dict) else None
if not isinstance(created_at, (int, float)) or isinstance(created_at, bool):
if _older_than(path, moment, UNREADABLE_KEEP_SECONDS):
_unlink(path, stats)
continue
try:
ttl = float(ttl_for_record(record))
except (TypeError, ValueError):
stats["errors"] += 1
continue
if moment - float(created_at) > ttl:
_unlink(path, stats)
return stats
def _prune_response_cache(moment: float) -> dict[str, int]:
adapter = _adapter()
return _prune_created_at_dir(
Path(adapter.ARTIFACT_DIR) / "response_cache",
moment,
lambda _record: adapter._cache_ttl_seconds(),
)
def _prune_snapshot_semantic_cache(moment: float) -> dict[str, int]:
adapter = _adapter()
def ttl_for(record: dict) -> float:
if record.get("negative") is True:
return adapter._official_full_snapshot_negative_ttl_seconds()
return adapter._official_full_snapshot_cache_ttl_seconds()
return _prune_created_at_dir(
Path(adapter.ARTIFACT_DIR) / "official_full_snapshot_cache",
moment,
ttl_for,
)
def _audit_keep_seconds() -> float:
raw = os.environ.get("VEDASTRO_AUDIT_ARTIFACT_KEEP_DAYS", "7").strip()
try:
days = float(raw) if raw else 7.0
except ValueError:
days = 7.0
return max(0.0, days) * 86400.0
def _prune_audit_artifacts(moment: float) -> dict[str, int]:
adapter = _adapter()
stats = _blank()
directory = Path(adapter.ARTIFACT_DIR)
if not directory.is_dir():
return stats
enabled = adapter._audit_artifacts_enabled()
max_age = _audit_keep_seconds() if enabled else UNREADABLE_KEEP_SECONDS
for path in directory.glob("*.json"):
if not path.is_file() or not _ARTIFACT_JSON.match(path.name):
continue
stats["scanned"] += 1
if _older_than(path, moment, max_age):
_unlink(path, stats)
return stats
def _parse_day(value: Any):
if not isinstance(value, str):
return None
raw = value[:10]
try:
return datetime.strptime(raw, "%Y-%m-%d").date()
except ValueError:
return None
def _prune_reference_dated(directory: Path, moment: float, *, earliest_kept) -> dict[str, int]:
stats = _blank()
if not directory.is_dir():
return stats
for path in directory.glob("*.json"):
if not _HEX64_JSON.match(path.name):
continue
stats["scanned"] += 1
try:
record = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError:
if _older_than(path, moment, UNREADABLE_KEEP_SECONDS):
_unlink(path, stats)
continue
except OSError:
stats["errors"] += 1
continue
ref = _parse_day(record.get("reference_date") if isinstance(record, dict) else None)
if ref is None:
if _older_than(path, moment, UNREADABLE_KEEP_SECONDS):
_unlink(path, stats)
continue
if ref < earliest_kept:
_unlink(path, stats)
return stats
def _prune_vedastro_snapshot_cache(moment: float) -> dict[str, int]:
from vedastro_snapshot_cache import MAX_STALE_DAYS
today = datetime.fromtimestamp(moment, timezone.utc).date()
earliest = today - timedelta(days=int(MAX_STALE_DAYS) + 1)
raw = os.environ.get("JYOTISH_VEDASTRO_SNAPSHOT_CACHE_DIR", "").strip()
directory = Path(raw) if raw else _repo_root() / "scratch" / "local" / "vedastro_snapshot_cache"
return _prune_reference_dated(directory, moment, earliest_kept=earliest)
def _prune_vedastro_negative_cache(moment: float) -> dict[str, int]:
today = datetime.fromtimestamp(moment, timezone.utc).date()
raw = os.environ.get("JYOTISH_VEDASTRO_SNAPSHOT_NEGATIVE_CACHE_DIR", "").strip()
directory = Path(raw) if raw else _repo_root() / "scratch" / "local" / "vedastro_snapshot_negative_cache"
return _prune_reference_dated(directory, moment, earliest_kept=today)
def _parse_updated_at(value: Any) -> float | None:
if not isinstance(value, str) or not value.strip():
return None
text = value.strip().replace("Z", "+00:00")
try:
parsed = datetime.fromisoformat(text)
except ValueError:
return None
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
return parsed.timestamp()
def _gateway_keep_seconds() -> float:
raw = os.environ.get("VEDASTRO_GATEWAY_JOB_KEEP_SECONDS", "86400").strip()
try:
return max(0.0, float(raw) if raw else 86400.0)
except ValueError:
return 86400.0
def _prune_gateway_jobs(moment: float) -> dict[str, int]:
stats = _blank()
raw = os.environ.get("VEDASTRO_GATEWAY_QUEUE_DIR", "").strip()
directory = Path(raw).expanduser() if raw else _repo_root() / "scratch" / "local" / "vedastro_gateway_jobs"
if not directory.is_dir():
return stats
keep = _gateway_keep_seconds()
for path in directory.glob("*.json"):
if not _GATEWAY_JOB_JSON.match(path.name):
continue
stats["scanned"] += 1
try:
record = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError:
continue
except OSError:
stats["errors"] += 1
continue
if not isinstance(record, dict):
continue
status = str(record.get("status") or "")
if status not in {"completed", "failed"}:
continue
updated = _parse_updated_at(record.get("updated_at"))
if updated is None or moment - updated <= keep:
continue
companion = directory / f"{path.stem}.official_raw_response.json"
_unlink(path, stats)
if companion.is_file():
stats["scanned"] += 1
_unlink(companion, stats)
return stats
def _varga_keep_seconds() -> float:
raw = os.environ.get("JYOTISH_VARGA_SEGMENTS_KEEP_DAYS", "7").strip()
try:
days = float(raw) if raw else 7.0
except ValueError:
days = 7.0
return max(0.0, days) * 86400.0
def _prune_varga_segments(moment: float) -> dict[str, int]:
stats = _blank()
raw = os.environ.get("JYOTISH_CACHE_DIR", "").strip()
root = Path(raw) if raw else _repo_root() / "scratch" / "local"
directory = root / "varga_segments"
if not directory.is_dir():
return stats
max_age = _varga_keep_seconds()
for path in directory.glob("*.json"):
if not _HEX64_JSON.match(path.name):
continue
stats["scanned"] += 1
if _older_than(path, moment, max_age):
_unlink(path, stats)
return stats
def _stamp(stats: dict[str, int], started: float) -> dict[str, Any]:
return {
"scanned": int(stats.get("scanned", 0) or 0),
"removed": int(stats.get("removed", 0) or 0),
"errors": int(stats.get("errors", 0) or 0),
"elapsed_ms": round((time.perf_counter() - started) * 1000, 3),
}
def prune_all(now: float | None = None, api_module=None) -> dict[str, Any]:
"""Prune every registered directory. Exceptions become error counts."""
moment = time.time() if now is None else float(now)
api = _api_module(api_module)
started = time.perf_counter()
directories: dict[str, dict[str, Any]] = {}
def _run(name: str, func: Callable[[float], dict[str, int]]) -> None:
t0 = time.perf_counter()
try:
stats = func(moment)
except Exception:
stats = {"scanned": 0, "removed": 0, "errors": 1}
directories[name] = _stamp(stats, t0)
_run("api_chart_cache", lambda moment: _prune_chart_cache(moment, api))
job_started = time.perf_counter()
try:
job = prune_async_job_records(moment, api)
job_dirs = job.get("directories") or {}
except Exception:
job_dirs = {
"high_rigor_jobs": {"scanned": 0, "removed": 0, "errors": 1},
"api_chart_response_jobs": _blank(),
}
job_elapsed = round((time.perf_counter() - job_started) * 1000, 3)
for name, stats in job_dirs.items():
stamped = _stamp(stats, job_started)
stamped["elapsed_ms"] = job_elapsed
directories[name] = stamped
_run("vedastro_response_cache", _prune_response_cache)
_run("vedastro_official_full_snapshot_cache", _prune_snapshot_semantic_cache)
_run("vedastro_audit_artifacts", _prune_audit_artifacts)
_run("vedastro_snapshot_cache", _prune_vedastro_snapshot_cache)
_run("vedastro_snapshot_negative_cache", _prune_vedastro_negative_cache)
_run("vedastro_gateway_jobs", _prune_gateway_jobs)
_run("varga_segments", _prune_varga_segments)
return {
"scanned": sum(item["scanned"] for item in directories.values()),
"removed": sum(item["removed"] for item in directories.values()),
"errors": sum(item["errors"] for item in directories.values()),
"elapsed_ms": round((time.perf_counter() - started) * 1000, 3),
"directories": directories,
}
def start_periodic_prune(interval_s: float | None = None):
"""Daemon thread. interval <= 0 starts nothing. Startup prune is separate."""
if interval_s is None:
raw = os.environ.get("JYOTISH_SCRATCH_PRUNE_INTERVAL_SECONDS", "1800").strip()
try:
interval_s = float(raw) if raw else 1800.0
except ValueError:
interval_s = 1800.0
if interval_s <= 0:
return None
def _loop() -> None:
while True:
time.sleep(interval_s)
try:
prune_all()
except Exception:
continue
thread = threading.Thread(target=_loop, name=_PRUNE_THREAD_NAME, daemon=True)
thread.start()
return thread
+23 -47
View File
@@ -2590,24 +2590,17 @@ def _vedastro_runtime_fingerprint() -> dict:
def _build_api_chart_cache_payload(body: dict) -> dict:
import api_scratch_retention as retention
today = body.get('today') or body.get('current_date')
return {
'cache_schema_version': 5,
'birth': {
'year': body.get('year'),
'month': body.get('month'),
'day': body.get('day'),
'hour': body.get('hour'),
'minute': body.get('minute'),
'second': body.get('second', 0),
'lat': body.get('lat'),
'lon': body.get('lon'),
'tz': body.get('tz'),
},
'cache_schema_version': 6,
'birth': retention.normalize_chart_cache_birth(body),
'calculation': {
'ayanamsa': _request_ayanamsa(body),
'node_mode': body.get('node_mode', body.get('nodeMode', 'mean')),
'today': body.get('today') or body.get('current_date'),
'transit_date': body.get('transit_date'),
'today': retention.normalize_chart_cache_day(today),
'transit_date': retention.normalize_chart_cache_day(body.get('transit_date')),
'skip_vedastro_main_entry_overview': bool(body.get('skip_vedastro_main_entry_overview')),
},
'vedastro_runtime': _vedastro_runtime_fingerprint(),
@@ -2678,14 +2671,15 @@ def _store_api_chart_response_cache(cache_payload: dict, payload: dict) -> dict:
)
ttl_seconds = _api_chart_cache_ttl_seconds()
if ttl_seconds > 0:
record = {
'cache_key': cache_key,
'created_at': created_at_unix,
'payload': payload_with_cache,
}
_api_chart_cache_path(cache_key).write_text(
json.dumps(record, ensure_ascii=False, sort_keys=True),
encoding='utf-8',
import api_scratch_retention as retention
retention.write_private_json(
_api_chart_cache_path(cache_key),
{
'cache_key': cache_key,
'created_at': created_at_unix,
'payload': payload_with_cache,
},
)
return payload_with_cache
@@ -2738,30 +2732,9 @@ def _sqlite_job_connection() -> sqlite3.Connection:
def prune_expired_async_jobs() -> dict:
"""Best-effort startup cleanup for local job records; never reads payloads."""
removed = 0
scanned = 0
if _async_job_backend() == "sqlite":
with _sqlite_job_connection() as connection:
scanned = connection.execute("SELECT COUNT(*) FROM async_jobs").fetchone()[0]
removed = connection.execute(
"DELETE FROM async_jobs WHERE expires_at IS NOT NULL AND expires_at <= ?", (time.time(),)
).rowcount
return {'scope': 'async_job_cleanup', 'scanned': scanned, 'removed': removed}
for scope in (_HIGH_RIGOR_JOB_SCOPE, _API_CHART_CACHE_SCOPE):
directory = _async_job_dir(scope)
if not directory.is_dir():
continue
for path in directory.glob('*.json'):
scanned += 1
try:
record = json.loads(path.read_text(encoding='utf-8'))
expires_at = record.get('expires_at_unix') if isinstance(record, dict) else None
if isinstance(expires_at, (int, float)) and time.time() >= float(expires_at):
path.unlink()
removed += 1
except (OSError, json.JSONDecodeError):
continue
return {'scope': 'async_job_cleanup', 'scanned': scanned, 'removed': removed}
import api_scratch_retention as retention
return retention.prune_async_job_records(api_module=sys.modules[__name__])
def _new_async_job_identity(prefix: str) -> dict:
@@ -10902,7 +10875,10 @@ def _parse_allowed_hosts(value):
def start_server(port=5200, host='127.0.0.1', allowed_origins=None, allowed_hosts=None):
cleanup = prune_expired_async_jobs()
import api_scratch_retention as retention
cleanup = retention.prune_all(api_module=sys.modules[__name__])
retention.start_periodic_prune()
server = ThreadingHTTPServer((host, port), JyotishAPIHandler)
server.daemon_threads = True
server.allowed_origins = allowed_origins or DEFAULT_ALLOWED_ORIGINS
@@ -0,0 +1,28 @@
"""Byte-compare numeric chart-cache spellings. See api_disk_cache_ab.py."""
from __future__ import annotations
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import api_disk_cache_ab as ab
def main() -> dict:
parent_error = None
try:
parent = ab._load_parent()
except Exception as exc:
parent = None
parent_error = exc.__class__.__name__
report = ab.run_byte_diff(parent or ab.current)
report["parent_import"] = parent_error or "ok"
report["parent"] = ab.PARENT
return report
if __name__ == "__main__":
json.dump(main(), sys.stdout, ensure_ascii=False, indent=2)
sys.stdout.write("\n")
+442
View File
@@ -0,0 +1,442 @@
"""A/B for chart-cache keys, retention, and response bytes.
Parent functions come from git show of the branch base (origin/staging at
branch creation), not from the task text's older baseline. Scratch files
stay in a temporary directory.
"""
from __future__ import annotations
import importlib.util
import json
import os
import socket
import statistics
import subprocess
import sys
import tempfile
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
PARENT = "66cd60081f37a57680274633fcb3fb942d01bb2f"
os.environ.setdefault("JYOTISH_SKIP_LOCAL_ENV", "1")
os.environ.setdefault("VEDASTRO_ENABLE_NETWORK", "0")
_SDK_PRESENT = importlib.util.find_spec("vedastro") is not None
def _network_reachable() -> bool:
try:
sock = socket.create_connection(("api.vedastro.org", 443), 3)
except OSError:
return False
sock.close()
return True
_NETWORK = _network_reachable()
class _Block:
def find_spec(self, name, path=None, target=None):
if name.split(".")[0] in {"jhora", "vedastro"}:
raise ModuleNotFoundError(f"No module named '{name}'")
return None
if "--with-sdk" not in sys.argv:
sys.meta_path.insert(0, _Block())
sys.path.insert(0, str(ROOT / "scripts"))
sys.path.insert(0, str(ROOT))
import api_scratch_retention as retention # noqa: E402
import jyotish_api_server as current # noqa: E402
import vedastro_service_adapter as adapter # noqa: E402
def _load_parent():
raw = subprocess.check_output(["git", "show", f"{PARENT}:scripts/jyotish_api_server.py"], text=True)
raw = raw.replace(
"SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))",
f"SCRIPTS_DIR = {str(ROOT / 'scripts')!r}",
)
raw = raw.replace(
"REPO_ROOT = os.path.abspath(os.path.join(SCRIPTS_DIR, '..'))",
f"REPO_ROOT = {str(ROOT)!r}",
)
directory = Path(tempfile.mkdtemp(prefix="jyotish-cache-a-"))
path = directory / "jyotish_api_server_parent.py"
path.write_text(raw, encoding="utf-8")
spec = importlib.util.spec_from_file_location("jyotish_api_server_parent", path)
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def _handler(module):
cls = module.JyotishAPIHandler
return cls.__new__(cls)
def _strip(payload: dict) -> str:
cloned = json.loads(json.dumps(payload, default=str))
cloned.pop("runtime_cache", None)
return json.dumps(cloned, ensure_ascii=False, sort_keys=True, default=str)
def _diff_paths(left: str, right: str, limit: int = 12) -> list[str]:
a = json.loads(left)
b = json.loads(right)
found: list[str] = []
def walk(one, two, prefix: str) -> None:
if len(found) >= limit:
return
if type(one) is not type(two):
found.append(prefix or "$")
return
if isinstance(one, dict):
for key in sorted(set(one) | set(two)):
if key not in one or key not in two:
found.append(f"{prefix}.{key}")
continue
walk(one[key], two[key], f"{prefix}.{key}")
elif isinstance(one, list):
if len(one) != len(two):
found.append(f"{prefix}[]")
return
for index, (item, other) in enumerate(zip(one, two)):
walk(item, other, f"{prefix}[{index}]")
elif one != two:
found.append(prefix or "$")
walk(a, b, "")
return found
def _body(hour, lat, **extra) -> dict:
payload = {
"year": 1990,
"month": 1,
"day": 1,
"minute": 0,
"lon": 20.0,
"tz": 5.5,
"hour": hour,
"lat": lat,
"today": "2026-10-14",
}
payload.update(extra)
return payload
def run_byte_diff(parent=None) -> dict:
os.environ["JYOTISH_API_CHART_CACHE_TTL_SECONDS"] = "0"
os.environ["VEDASTRO_WRITE_AUDIT_ARTIFACTS"] = "0"
parent = parent or current
groups = {
"hour": [_body(12, 10.0), _body(12.0, 10.0), _body("12", 10.0)],
"lat": [_body(12, 10.0), _body(12, "10.0")],
"tz": [_body(12, 10.0), _body(12, 10.0, tz="5.5")],
"second": [_body(12, 10.0), _body(12, 10.0, second=0)],
}
report = {"groups": {}, "stable_repeat": None}
handler = _handler(current)
started = time.perf_counter()
try:
sample = handler._compute_chart_sync(_body(12, 10.0, skip_vedastro_main_entry_overview=True))
except Exception as exc:
return {"ok": False, "error": exc.__class__.__name__, "elapsed_s": round(time.perf_counter() - started, 3)}
sample_s = round(time.perf_counter() - started, 3)
repeat = handler._compute_chart_sync(_body(12, 10.0, skip_vedastro_main_entry_overview=True))
report["stable_repeat"] = _strip(sample) == _strip(repeat)
report["sample_seconds"] = sample_s
if not report["stable_repeat"]:
report["volatile_paths"] = _diff_paths(_strip(sample), _strip(repeat))
for name, bodies in groups.items():
for skip in (True, False):
rendered = []
for body in bodies:
item = dict(body)
if skip:
item["skip_vedastro_main_entry_overview"] = True
rendered.append(_strip(handler._compute_chart_sync(item)))
same = len(set(rendered)) == 1
entry = {"skip": skip, "same": same}
if not same:
entry["paths"] = _diff_paths(rendered[0], rendered[1])
clocks = ("cache_created_at", "cache_expires_at", "called_at")
entry["only_clock_fields"] = bool(entry["paths"]) and all(
path.endswith(clocks) for path in entry["paths"]
)
else:
entry["only_clock_fields"] = False
report["groups"][f"{name}:skip={skip}"] = entry
if parent is not current:
a_handler = _handler(parent)
b_handler = handler
body = _body(12, 10.0, skip_vedastro_main_entry_overview=True)
a_text = _strip(a_handler._compute_chart_sync(body))
b_text = _strip(b_handler._compute_chart_sync(body))
report["a_vs_b_same"] = a_text == b_text
if a_text != b_text:
report["a_vs_b_paths"] = _diff_paths(a_text, b_text)
report["ok"] = all(item["same"] or item.get("only_clock_fields") for item in report["groups"].values())
return report
def _requests():
charts = []
for index in range(20):
charts.append(
{
"year": 1990,
"month": 1,
"day": 1 + (index % 27),
"minute": index % 60,
"lon": float(index),
"tz": 5.5,
}
)
spellings = (
{"hour": 12, "lat": 10.0},
{"hour": 12.0, "lat": "10.0"},
{"hour": "12", "lat": 10.0},
)
days = [f"2026-10-{day:02d}" for day in range(8, 15)]
rows = []
for chart in charts:
for spelling in spellings:
for skip in (False, True):
for day in days:
body = dict(chart)
body.update(spelling)
body["today"] = day
if skip:
body["skip_vedastro_main_entry_overview"] = True
rows.append(body)
return rows
def _key(module, body) -> str:
return module._api_chart_cache_key(module._build_api_chart_cache_payload(body))
def _hit_rate(module, rows) -> dict:
seen = set()
hits = 0
for body in rows:
for _repeat in range(3):
key = _key(module, body)
if key in seen:
hits += 1
else:
seen.add(key)
total = len(rows) * 3
return {"requests": total, "unique_keys": len(seen), "hits": hits, "hit_rate": round(hits / total, 4)}
def _seed_and_prune(module, rows) -> dict:
now = time.time()
os.environ["JYOTISH_API_CHART_CACHE_TTL_SECONDS"] = "900"
scratch = Path(tempfile.mkdtemp(prefix="jyotish-cache-b-"))
chart = scratch / "api_chart_cache"
jobs = scratch / "jobs"
adapter_dir = scratch / "adapter"
chart.mkdir()
jobs.mkdir()
current._api_chart_cache_dir = lambda: chart
current._async_job_dir = lambda scope: jobs
current._async_job_backend = lambda: "file"
adapter.ARTIFACT_DIR = adapter_dir
os.environ["JYOTISH_VEDASTRO_SNAPSHOT_CACHE_DIR"] = str(scratch / "snap")
os.environ["JYOTISH_VEDASTRO_SNAPSHOT_NEGATIVE_CACHE_DIR"] = str(scratch / "neg")
os.environ["VEDASTRO_GATEWAY_QUEUE_DIR"] = str(scratch / "gw")
os.environ["JYOTISH_CACHE_DIR"] = str(scratch / "cache")
days = sorted({body["today"] for body in rows})
newest = days[-1]
written = {}
for body in rows:
key = _key(module, body)
age_days = days.index(body["today"])
age_days = len(days) - 1 - age_days
written[key] = now - age_days * 86400
for key, created in written.items():
path = chart / f"{key}.json"
path.write_text(json.dumps({"cache_key": key, "created_at": created, "payload": {}}), encoding="utf-8")
before = len(list(chart.glob("*.json")))
before_bytes = sum(path.stat().st_size for path in chart.glob("*.json"))
today_keys = len({_key(module, body) for body in rows if body["today"] == newest})
if module is current:
prune = retention.prune_all(now=now)
after = len(list(chart.glob("*.json")))
elapsed = prune["directories"]["api_chart_cache"]["elapsed_ms"]
else:
after = before
elapsed = None
return {
"files_before": before,
"bytes_before": before_bytes,
"files_after": after,
"today_active_keys": today_keys,
"chart_prune_ms": elapsed,
}
def _artifact_counts() -> dict:
scratch = Path(tempfile.mkdtemp(prefix="jyotish-artifact-ab-"))
adapter.ARTIFACT_DIR = scratch
os.environ.pop("VEDASTRO_WRITE_AUDIT_ARTIFACTS", None)
result = {
"operation": "official_full_snapshot",
"source_metadata": {
"operation": "official_full_snapshot",
"request_hash": "a" * 64,
"response_hash": "b" * 64,
},
}
adapter._write_artifact(result)
return {"b_artifact_files": len(list(scratch.glob("*.json")))}
def _negative_cache_after_prune() -> dict:
scratch = Path(tempfile.mkdtemp(prefix="jyotish-neg-ab-"))
os.environ["JYOTISH_VEDASTRO_SNAPSHOT_NEGATIVE_CACHE_DIR"] = str(scratch)
os.environ["JYOTISH_VEDASTRO_SNAPSHOT_CACHE_DIR"] = str(scratch / "unused-snap")
os.environ["VEDASTRO_GATEWAY_QUEUE_DIR"] = str(scratch / "unused-gw")
os.environ["JYOTISH_CACHE_DIR"] = str(scratch / "unused-cache")
current._api_chart_cache_dir = lambda: scratch / "unused-chart"
current._async_job_dir = lambda scope: scratch / "unused-jobs"
(scratch / "unused-chart").mkdir()
(scratch / "unused-jobs").mkdir()
today = time.strftime("%Y-%m-%d", time.gmtime())
yesterday = time.strftime("%Y-%m-%d", time.gmtime(time.time() - 86400))
for label, day in (("a" * 64, today), ("b" * 64, yesterday)):
path = scratch / f"{label}.json"
path.write_text(json.dumps({"reference_date": day}), encoding="utf-8")
retention.prune_all(now=time.time())
return {"files_after": len(list(scratch.glob("*.json")))}
def _prune_10k() -> dict:
scratch = Path(tempfile.mkdtemp(prefix="jyotish-prune-10k-"))
chart = scratch / "chart"
chart.mkdir()
current._api_chart_cache_dir = lambda: chart
current._async_job_dir = lambda scope: scratch / "jobs"
(scratch / "jobs").mkdir()
current._async_job_backend = lambda: "file"
adapter.ARTIFACT_DIR = scratch / "adapter"
os.environ["JYOTISH_API_CHART_CACHE_TTL_SECONDS"] = "900"
os.environ["JYOTISH_VEDASTRO_SNAPSHOT_CACHE_DIR"] = str(scratch / "snap")
os.environ["JYOTISH_VEDASTRO_SNAPSHOT_NEGATIVE_CACHE_DIR"] = str(scratch / "neg")
os.environ["VEDASTRO_GATEWAY_QUEUE_DIR"] = str(scratch / "gw")
os.environ["JYOTISH_CACHE_DIR"] = str(scratch / "cache")
for index in range(10000):
path = chart / f"{index:064x}.json"
path.write_text('{"created_at":1,"payload":{}}', encoding="utf-8")
started = time.perf_counter()
result = retention.prune_all(now=time.time())
elapsed = time.perf_counter() - started
return {
"prune_all_seconds": round(elapsed, 3),
"chart_removed": result["directories"]["api_chart_cache"]["removed"],
"chart_elapsed_ms": result["directories"]["api_chart_cache"]["elapsed_ms"],
"files_left": len(list(chart.glob("*.json"))),
}
def _one_chart_timing() -> dict:
os.environ["JYOTISH_API_CHART_CACHE_TTL_SECONDS"] = "0"
handler = _handler(current)
body = _body(12, 10.0, skip_vedastro_main_entry_overview=True)
misses = []
for _index in range(3):
started = time.perf_counter()
handler._compute_chart_sync(body)
misses.append((time.perf_counter() - started) * 1000)
os.environ["JYOTISH_API_CHART_CACHE_TTL_SECONDS"] = "900"
scratch = Path(tempfile.mkdtemp(prefix="jyotish-hit-time-"))
current._api_chart_cache_dir = lambda: scratch
cold = handler._compute_chart_sync(body)
hits = []
for _index in range(3):
started = time.perf_counter()
hit = handler._compute_chart_sync(body)
hits.append((time.perf_counter() - started) * 1000)
assert hit.get("runtime_cache", {}).get("cache_hit") is True
return {
"miss_median_ms": round(statistics.median(misses), 3),
"hit_median_ms": round(statistics.median(hits), 3),
"cold_has_runtime_cache": isinstance(cold, dict) and "runtime_cache" in cold,
}
def main() -> dict:
parent_error = None
try:
parent = _load_parent()
except Exception as exc:
parent = None
parent_error = exc.__class__.__name__
rows = _requests()
side_a = _hit_rate(parent, rows) if parent is not None else None
side_b = _hit_rate(current, rows)
report = {
"parent": PARENT,
"parent_import": parent_error or "ok",
"vedastro_sdk": _SDK_PRESENT,
"vedastro_network": _NETWORK,
"hit_rate_a": side_a,
"hit_rate_b": side_b,
"artifacts": _artifact_counts(),
"negative_cache_files_after_prune": _negative_cache_after_prune(),
"prune_10k": _prune_10k(),
}
if side_a and side_a["hit_rate"]:
report["hit_rate_ratio_b_over_a"] = round(side_b["hit_rate"] / side_a["hit_rate"], 3)
try:
report["files_b"] = _seed_and_prune(current, rows)
if parent is not None:
report["files_a"] = _seed_and_prune(parent, rows)
except Exception as exc:
report["files_error"] = exc.__class__.__name__
try:
report["timing"] = _one_chart_timing()
except Exception as exc:
report["timing_error"] = exc.__class__.__name__
try:
report["byte_diff"] = run_byte_diff(parent or current)
except Exception as exc:
report["byte_diff"] = {"ok": False, "error": exc.__class__.__name__}
return report
def _sdk_probe() -> dict:
os.environ["JYOTISH_API_CHART_CACHE_TTL_SECONDS"] = "0"
os.environ["VEDASTRO_ENABLE_NETWORK"] = "0"
os.environ["VEDASTRO_WRITE_AUDIT_ARTIFACTS"] = "0"
handler = _handler(current)
body = _body(12, 10.0)
started = time.perf_counter()
try:
handler._compute_chart_sync(body)
error = None
except Exception as exc:
error = type(exc).__name__
return {
"blocked_vedastro": False,
"vedastro_sdk": _SDK_PRESENT,
"vedastro_network": _NETWORK,
"network_enabled": False,
"elapsed_s": round(time.perf_counter() - started, 3),
"error": error,
}
if __name__ == "__main__":
payload = _sdk_probe() if "--sdk-probe" in sys.argv else main()
json.dump(payload, sys.stdout, ensure_ascii=False, indent=2)
sys.stdout.write("\n")
+26 -1
View File
@@ -340,6 +340,30 @@ def probe_official_rest_health(*, force_refresh: bool = True) -> dict[str, Any]:
return result
def _gateway_snapshot_ttl_helpers():
try:
from vedastro_service_adapter import (
_official_full_snapshot_cache_ttl_seconds,
_official_full_snapshot_negative_ttl_seconds,
)
except ModuleNotFoundError:
from scripts.vedastro_service_adapter import (
_official_full_snapshot_cache_ttl_seconds,
_official_full_snapshot_negative_ttl_seconds,
)
return _official_full_snapshot_cache_ttl_seconds, _official_full_snapshot_negative_ttl_seconds
def _gateway_official_snapshot_ttl_seconds() -> float:
positive, _negative = _gateway_snapshot_ttl_helpers()
return positive()
def _gateway_official_snapshot_negative_ttl_seconds() -> float:
_positive, negative = _gateway_snapshot_ttl_helpers()
return negative()
def gateway_status(*, force_refresh: bool = True) -> dict[str, Any]:
from scripts.diagnose_vedastro_mode import build_report as build_vedastro_mode_report
@@ -368,7 +392,8 @@ def gateway_status(*, force_refresh: bool = True) -> dict[str, Any]:
),
"sdk_version": _sdk_version(),
"cache_ttl_seconds": config["cache_ttl_seconds"],
"official_full_snapshot_cache_ttl_seconds": _int_env("VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS", 0),
"official_full_snapshot_cache_ttl_seconds": _gateway_official_snapshot_ttl_seconds(),
"official_full_snapshot_negative_cache_ttl_seconds": _gateway_official_snapshot_negative_ttl_seconds(),
"queue_enabled": config["queue_enabled"],
"fail_open_local": config["fail_open_local"],
"official_readiness": {
+61 -16
View File
@@ -425,14 +425,23 @@ def _repo_relative(path: Path) -> str:
return str(path)
def _write_artifact(result: dict[str, Any]) -> str:
def _audit_artifacts_enabled() -> bool:
raw = os.environ.get("VEDASTRO_WRITE_AUDIT_ARTIFACTS", "0").strip().lower()
return raw in {"1", "true", "yes", "on"}
def _write_artifact(result: dict[str, Any]) -> str | None:
if not _audit_artifacts_enabled():
return None
metadata = result.get("source_metadata") or {}
artifact = _artifact_path(
str(metadata.get("operation") or result.get("operation") or "calculation"),
str(metadata.get("request_hash") or "no-request-hash"),
str(metadata.get("response_hash") or "no-response-hash"),
)
artifact.write_text(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8")
from api_scratch_retention import write_private_json
write_private_json(artifact, result)
return _repo_relative(artifact)
@@ -486,6 +495,22 @@ def _official_full_snapshot_cache_ttl_seconds() -> float:
return 0.0
def _official_full_snapshot_negative_ttl_seconds() -> float:
normal = _official_full_snapshot_cache_ttl_seconds()
if normal <= 0:
return 0.0
raw = os.environ.get("VEDASTRO_OFFICIAL_FULL_SNAPSHOT_NEGATIVE_TTL_SECONDS", "300").strip()
try:
negative = max(0.0, float(raw)) if raw else 300.0
except ValueError:
negative = 300.0
return min(negative, normal)
def _official_full_snapshot_result_is_success(payload: dict[str, Any]) -> bool:
return payload.get("available") is True and payload.get("status") == "ok"
def _free_tier_max_requests() -> int:
raw = os.environ.get(FREE_TIER_MAX_REQUESTS_ENV, "").strip()
if not raw:
@@ -1719,9 +1744,11 @@ def _attach_official_full_snapshot_semantic_cache_metadata(
cache_key: str,
created_at_unix: float,
cache_hit: bool,
ttl_seconds: float | None = None,
) -> dict[str, Any]:
payload_copy = json.loads(json.dumps(payload))
ttl_seconds = _official_full_snapshot_cache_ttl_seconds()
if ttl_seconds is None:
ttl_seconds = _official_full_snapshot_cache_ttl_seconds()
created_at = datetime.utcfromtimestamp(created_at_unix).strftime("%Y-%m-%dT%H:%M:%SZ")
expires_at = datetime.utcfromtimestamp(created_at_unix + ttl_seconds).strftime("%Y-%m-%dT%H:%M:%SZ")
metadata = dict(payload_copy.get("source_metadata") or {})
@@ -1745,9 +1772,15 @@ def _rebind_official_full_snapshot_case_identity(payload: dict[str, Any], case_i
return payload_copy
def _semantic_cache_ttl_seconds(record: dict[str, Any]) -> float:
# Records written before the negative flag are successes and keep the long TTL.
if record.get("negative") is True:
return _official_full_snapshot_negative_ttl_seconds()
return _official_full_snapshot_cache_ttl_seconds()
def _load_official_full_snapshot_semantic_cache(case: dict[str, Any], case_id: str) -> dict[str, Any] | None:
ttl_seconds = _official_full_snapshot_cache_ttl_seconds()
if ttl_seconds <= 0:
if _official_full_snapshot_cache_ttl_seconds() <= 0:
return None
cache_key = _official_full_snapshot_cache_key(case, case_id)
cache_path = _official_full_snapshot_cache_path(cache_key)
@@ -1763,7 +1796,8 @@ def _load_official_full_snapshot_semantic_cache(case: dict[str, Any], case_id: s
payload = record.get("payload")
if not isinstance(created_at_raw, (int, float)) or not isinstance(payload, dict):
return None
if time.time() - float(created_at_raw) > ttl_seconds:
ttl_seconds = _semantic_cache_ttl_seconds(record)
if ttl_seconds <= 0 or time.time() - float(created_at_raw) > ttl_seconds:
return None
rebound = _rebind_official_full_snapshot_case_identity(payload, case_id)
return _attach_official_full_snapshot_semantic_cache_metadata(
@@ -1771,28 +1805,37 @@ def _load_official_full_snapshot_semantic_cache(case: dict[str, Any], case_id: s
cache_key=cache_key,
created_at_unix=float(created_at_raw),
cache_hit=True,
ttl_seconds=ttl_seconds,
)
def _store_official_full_snapshot_semantic_cache(case: dict[str, Any], case_id: str, payload: dict[str, Any]) -> dict[str, Any]:
cache_key = _official_full_snapshot_cache_key(case, case_id)
created_at_unix = time.time()
negative = not _official_full_snapshot_result_is_success(payload)
ttl_seconds = (
_official_full_snapshot_negative_ttl_seconds()
if negative
else _official_full_snapshot_cache_ttl_seconds()
)
payload_copy = _attach_official_full_snapshot_semantic_cache_metadata(
payload,
cache_key=cache_key,
created_at_unix=created_at_unix,
cache_hit=False,
ttl_seconds=ttl_seconds,
)
ttl_seconds = _official_full_snapshot_cache_ttl_seconds()
if ttl_seconds > 0:
record = {
"cache_key": cache_key,
"created_at": created_at_unix,
"payload": payload_copy,
}
_official_full_snapshot_cache_path(cache_key).write_text(
json.dumps(record, ensure_ascii=False, sort_keys=True),
encoding="utf-8",
from api_scratch_retention import write_private_json
write_private_json(
_official_full_snapshot_cache_path(cache_key),
{
"cache_key": cache_key,
"created_at": created_at_unix,
"negative": negative,
"payload": payload_copy,
},
)
return payload_copy
@@ -1882,7 +1925,9 @@ def _store_cached_payload(cache_key: str, payload: dict[str, Any]) -> dict[str,
"created_at": created_at_unix,
"payload": payload_copy,
}
cache_path.write_text(json.dumps(record, ensure_ascii=False, sort_keys=True), encoding="utf-8")
from api_scratch_retention import write_private_json
write_private_json(cache_path, record)
return payload_copy