fix(sync): track cleanup state to stop re-deleting log rows and bound SyncQueue
- access_reader.delete_log_ids returns the actual rows deleted (was None). - sql_writer.mark_cleaned flips applied queue rows to 'cleaned' (sets CleanedAt) after their Access log rows are physically removed, so the same IDs are never deleted twice. - sql_writer.purge_cleaned removes 'cleaned' rows older than a retention window (default 24h) so SyncQueue stops growing without bound. - cleanup.cleanup_file marks rows cleaned after a successful delete and returns the real delete count, so the service log reports honest 'cleaned N' instead of a constant. - service.cycle calls purge_cleaned once per pass; config adds cleaned_retention_hours (default 24). - sql/01_sync_queue.sql adds CleanedAt column + IX_SyncQueue_Cleaned idempotently. - tests: unit coverage for mark_cleaned/purge_cleaned/delete_log_ids return count; assert cycle purges each pass.
This commit is contained in:
@@ -87,15 +87,18 @@ class AccessReader:
|
||||
|
||||
def delete_log_ids(
|
||||
self, ids: list[int], batch_size: int, retries: int
|
||||
) -> None:
|
||||
) -> int:
|
||||
"""Delete the given log-row IDs in chunks, retrying on lock contention.
|
||||
|
||||
A no-op when ``ids`` is empty (never raises). Retries with linear
|
||||
backoff because the live client may briefly hold a page lock on
|
||||
``TableChangeLog``.
|
||||
Returns the total number of rows actually deleted (sum of per-chunk
|
||||
``cursor.rowcount``), so the caller can report honest counts. A no-op
|
||||
(returns 0) when ``ids`` is empty. Retries with linear backoff because
|
||||
the live client may briefly hold a page lock on ``TableChangeLog``.
|
||||
Raises on final lock failure.
|
||||
"""
|
||||
if not ids:
|
||||
return
|
||||
return 0
|
||||
total = 0
|
||||
cur = self._connect().cursor()
|
||||
for i in range(0, len(ids), batch_size):
|
||||
chunk = ids[i:i + batch_size]
|
||||
@@ -106,12 +109,14 @@ class AccessReader:
|
||||
f"DELETE FROM TableChangeLog WHERE ID IN ({placeholders})",
|
||||
*chunk,
|
||||
)
|
||||
total += cur.rowcount
|
||||
break
|
||||
except pyodbc.OperationalError:
|
||||
if attempt < retries - 1:
|
||||
time.sleep(0.2 * (attempt + 1))
|
||||
else:
|
||||
raise
|
||||
return total
|
||||
|
||||
def close(self):
|
||||
if self._conn:
|
||||
|
||||
Reference in New Issue
Block a user