fix: rebuild trustworthy local-day evidence (#99)
This commit is contained in:
+26
-26
@@ -1,8 +1,9 @@
|
||||
"""Historical repair and retention (issue #74).
|
||||
"""Historical repair and retention (issue #74, #99).
|
||||
|
||||
Re-derives hour observations and day aggregates from surviving raw samples,
|
||||
with idempotent and interruption-safe guarantees. Preserves import markers,
|
||||
boundary anchors, and valid historical summaries.
|
||||
rebuilds legacy local-day evidence from surviving samples or trustworthy UTC
|
||||
hours, and preserves import markers, boundary anchors, and valid summaries.
|
||||
Repair is idempotent and interruption-safe.
|
||||
|
||||
Contracts:
|
||||
- Idempotent: running repair multiple times produces no duplicates
|
||||
@@ -71,7 +72,6 @@ def _set_repair_in_progress(conn: sqlite3.Connection, in_progress: bool) -> None
|
||||
"INSERT OR REPLACE INTO store_metadata (key, value) VALUES (?, ?)",
|
||||
("repair_in_progress", "true" if in_progress else "false"),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _update_repair_status(conn: sqlite3.Connection, result: RepairResult) -> None:
|
||||
@@ -100,8 +100,6 @@ def _update_repair_status(conn: sqlite3.Connection, result: RepairResult) -> Non
|
||||
"INSERT OR REPLACE INTO store_metadata (key, value) VALUES (?, ?)",
|
||||
("days_derived", str(days)),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
|
||||
|
||||
def get_repair_status(conn: sqlite3.Connection) -> RepairStatus:
|
||||
@@ -364,20 +362,16 @@ def repair_derivation(
|
||||
elif hasattr(clock, 'utcnow'):
|
||||
clock = clock.utcnow()
|
||||
|
||||
# Check if repair is already in progress
|
||||
if is_repair_in_progress(conn):
|
||||
return RepairResult(
|
||||
ok=False,
|
||||
error="Repair already in progress",
|
||||
)
|
||||
|
||||
# Mark repair as in progress
|
||||
_set_repair_in_progress(conn, True)
|
||||
|
||||
result = RepairResult(ok=True)
|
||||
|
||||
try:
|
||||
# Begin transaction
|
||||
# A committed in-progress marker may be stale after process death.
|
||||
# SQLite's write lock serializes active repairs; retry idempotently.
|
||||
if conn.in_transaction:
|
||||
conn.commit()
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
_set_repair_in_progress(conn, True)
|
||||
conn.commit()
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
|
||||
# 1. Find and derive intervals from sample pairs
|
||||
@@ -407,7 +401,12 @@ def repair_derivation(
|
||||
result.days_created += 1
|
||||
else:
|
||||
result.days_updated += 1
|
||||
|
||||
|
||||
# Rebuild only legacy or unavailable local-day activity. This shares
|
||||
# the migration derivation and leaves trustworthy published totals intact.
|
||||
from .local_day import repair_legacy_local_day_evidence
|
||||
repair_legacy_local_day_evidence(conn)
|
||||
|
||||
# 3. Count boundary anchors retained
|
||||
now = clock if isinstance(clock, datetime) else datetime.now(timezone.utc)
|
||||
cursor = conn.execute("SELECT ts FROM samples ORDER BY ts")
|
||||
@@ -428,21 +427,22 @@ def repair_derivation(
|
||||
)
|
||||
result.legacy_summaries_preserved = cursor.fetchone()[0]
|
||||
|
||||
# Commit transaction
|
||||
conn.commit()
|
||||
|
||||
# Update repair status
|
||||
# Publish derived rows and completion status together. A process
|
||||
# interruption rolls back the in-progress marker with the repair.
|
||||
_update_repair_status(conn, result)
|
||||
_set_repair_in_progress(conn, False)
|
||||
conn.commit()
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
try:
|
||||
_set_repair_in_progress(conn, False)
|
||||
conn.commit()
|
||||
except sqlite3.Error:
|
||||
conn.rollback()
|
||||
logger.error("Repair failed: %s", e)
|
||||
return RepairResult(
|
||||
ok=False,
|
||||
error=str(e),
|
||||
)
|
||||
finally:
|
||||
# Mark repair as complete
|
||||
_set_repair_in_progress(conn, False)
|
||||
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user