Skip to content

[Backup] Log backup rotation failures instead of silently swallowing them#305

Merged
bitflicker64 merged 3 commits into
bitflicker64:mainfrom
SAY-5:bugfix/backup-rotation-logging
Jul 20, 2026
Merged

[Backup] Log backup rotation failures instead of silently swallowing them#305
bitflicker64 merged 3 commits into
bitflicker64:mainfrom
SAY-5:bugfix/backup-rotation-logging

Conversation

@SAY-5

@SAY-5 SAY-5 commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR fixes a silent failure in backup_db()'s rotation block where except Exception: pass swallowed all errors without logging, potentially causing unbounded backup accumulation when disk-full, permission errors, or filesystem issues occurred. The change narrows the exception handler to OSError (which covers PermissionError and FileNotFoundError) and adds logger.exception() to surface failures while preserving the guarantee that rotation errors never crash the backup process.

Changes

Core

  • termstory/backup.py: Added import logging and module-level logger = logging.getLogger(__name__) for error logging
  • termstory/backup.py: Changed rotation block exception handler from broad except Exception: to specific except OSError: to catch filesystem-related errors while allowing other exceptions to propagate
  • termstory/backup.py: Replaced bare pass with logger.exception("Failed to rotate old backups in %s", backup_dir) to log rotation failures with full traceback
  • termstory/backup.py: Updated comment to clarify that rotation failures must be visible in logs while still not crashing the backup process

Tests

  • tests/test_backup.py: Added test_backup_survives_rotation_failure() to verify that backup operations succeed even when rotation fails due to PermissionError
  • tests/test_backup.py: Test uses MockDatetime to create 11 backups (exceeding the 10-backup rotation threshold) and mocks os.remove to raise PermissionError, then asserts the backup still returns a valid file path
  • tests/test_backup.py: Test captures logs with caplog and asserts a "Failed to rotate old backups" ERROR record is emitted, so a regression back to except OSError: pass would fail the test

Fixes

Breaking Changes

None

Testing

Run the backup test suite to verify the fix:

python3 -m pytest tests/test_backup.py -v

The new test test_backup_survives_rotation_failure specifically validates that:

  1. Backups succeed even when rotation fails
  2. The backup path is still returned correctly
  3. Rotation errors are caught and logged (asserted via caplog) without crashing the backup operation

Notes

The rotation logic maintains a 10-snapshot policy by deleting the oldest backups when the count exceeds 10 . This change ensures that filesystem errors during cleanup (such as disk-full conditions or permission issues) are now visible in logs while preserving the non-crashing contract that the backup operation itself always succeeds and returns a valid path.

…them

Signed-off-by: Sai Asish Y <say.apm35@gmail.com>
@git-miku git-miku Bot added the needs-triage Awaiting human triage — run /triage to mark as triaged label Jul 18, 2026
@git-miku git-miku Bot assigned SAY-5 Jul 18, 2026
@git-miku git-miku Bot added size/S PR changes ~38 lines area/storage area/storage kind/tests kind/tests labels Jul 18, 2026
@git-miku

git-miku Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Tip

👋 Hey @SAY-5 — Miku's on it. Here are the most useful commands for this PR:

💬 Command What it does
:miku /review 🔍 Line-by-line review
:miku /review-sc ✏️ Inline suggested changes
:miku /security 🛡️ Security scan
:miku /perf ⚡ Performance notes
:miku /test 🧪 Test suggestions
:miku /summarize 📝 TL;DR

Note

🎓 This repo is part of ECSOC26. Run :miku /ecsoc to add the ECSOC26 label to this PR.

Note

⭐ Star this repo to unlock all commands. Say :miku /help for the full list.

📖 All commands
🤖 AI-powered — 14 commands
Command Description Works on
:miku /ask <question> Answer a question about the codebase Issues & PRs
:miku /review Full code review (auto-fetches diff) PRs only
:miku /review-sc Post inline suggested-change comments PRs only
:miku /security OWASP security review (injection, auth, crypto…) PRs only
:miku /perf Performance & efficiency analysis PRs only
:miku /docs Generate docstrings for changed code PRs only
:miku /test Suggest unit tests for changed code PRs only
:miku /roast Humorous but constructive code roast 🔥 PRs only
:miku /checklist Smart diff-aware PR checklist PRs only
:miku /summarize TL;DR of this issue or PR Issues & PRs
:miku /explain <target> Explain a file, function, or concept Issues & PRs
:miku /changelog Draft a CHANGELOG entry PRs only
:miku /estimate Story-point effort estimate Issues only
:miku /chat <message> General conversation Issues & PRs
🔧 Issue & PR management — 18 commands
Command Description Works on
:miku /assign [@user] Assign to yourself or another user Issues & PRs
:miku /unassign [@user] Remove yourself or another user Issues & PRs
:miku /label <name> Add a label Issues & PRs
:miku /remove-label <name> Remove a label Issues & PRs
:miku /close [reason] Close this issue or PR Issues & PRs
:miku /reopen Reopen a closed issue or PR Issues & PRs
:miku /approve [message] Submit an approving review on a PR PRs only
:miku /retitle <new title> Change the title Issues & PRs
:miku /duplicate #N Mark as duplicate of #N and close Issues & PRs
:miku /milestone [name] Set or list open milestones Issues & PRs
:miku /ship [version] Mark PR as shipped / deployed PRs only
:miku /lock [reason] Lock conversation (resolved / off-topic / spam) Issues & PRs
:miku /unlock Unlock conversation Issues & PRs
:miku /cc @user1 [@user2] Request reviews PRs only
:miku /lgtm [cancel] Signal "Looks Good To Me" Issues & PRs
:miku /hold [cancel] Block merge; :miku /hold cancel releases it Issues & PRs
:miku /wip Toggle WIP (converts PR to draft) Issues & PRs
:miku /merge Merge this PR (CI + approval required) PRs only
🎉 Community & utility — 9 commands
Command Description Works on
:miku /vote Open a 👍 👎 🚀 reaction poll Issues & PRs
:miku /kudos @user [msg] Give public kudos to a contributor Issues & PRs
:miku /stats Repo stars, forks, top contributors Issues & PRs
:miku /remind <dur> <msg> Schedule a reminder (e.g. 2h, 30m, 1d) Issues & PRs
:miku /size Show PR size breakdown PRs only
:miku /ping [@user ...] Ping contributors with an AI message Issues & PRs
:miku /joke Tell a programming joke Issues & PRs
:miku /shrug Add ¯_(ツ)_/¯ label Issues & PRs
:miku /help Show this message Issues & PRs

@greptile-apps

greptile-apps Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR upgrades rotation-failure logging in backup_db() from logger.warning (no traceback) to logger.exception (ERROR level with full traceback), and adds a test that verifies the backup still succeeds when rotation fails. The change is narrowly scoped and preserves the non-crashing contract.

  • termstory/backup.py: except OSError as e: logger.warning(...)except OSError: logger.exception(...), updating the message to include backup_dir and emitting at ERROR level with a full traceback.
  • tests/test_backup.py: New test seeds 11 backups, patches os.remove to raise PermissionError, and asserts both a valid backup path and a logged ERROR record — closing the regression gap flagged in [Easy] backup.py: backup rotation failures swallowed by bare except Exception: pass #296.

Confidence Score: 5/5

Safe to merge — the change is confined to the logging call inside an already-isolated exception handler; no production logic is altered.

The only behavioral change is replacing a warning-level log (no traceback) with an exception-level log (full traceback). The rotation and backup paths are untouched, and the new test validates the core contract.

No files require special attention.

Important Files Changed

Filename Overview
termstory/backup.py Upgrades rotation-failure logging from logger.warning (no traceback) to logger.exception (ERROR level + full traceback) and updates the message to include backup_dir; no logic changes to the rotation or backup paths.
tests/test_backup.py Adds test_backup_survives_rotation_failure which correctly seeds 11 backups, patches os.remove to raise PermissionError, and asserts both the backup path and log emission; uses record.message instead of the reliable caplog.messages API for the log assertion.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[backup_db called] --> B[Copy DB via sqlite3.backup API]
    B --> C{Copy succeeded?}
    C -- No --> D[Exception propagates to caller]
    C -- Yes --> E[Glob existing backups, sort by name]
    E --> F{Count > 10?}
    F -- No --> G[Return backup_path]
    F -- Yes --> H[os.remove oldest backup]
    H --> I{OSError?}
    I -- No --> F
    I -- Yes --> J[logger.exception with backup_dir and full traceback]
    J --> G
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[backup_db called] --> B[Copy DB via sqlite3.backup API]
    B --> C{Copy succeeded?}
    C -- No --> D[Exception propagates to caller]
    C -- Yes --> E[Glob existing backups, sort by name]
    E --> F{Count > 10?}
    F -- No --> G[Return backup_path]
    F -- Yes --> H[os.remove oldest backup]
    H --> I{OSError?}
    I -- No --> F
    I -- Yes --> J[logger.exception with backup_dir and full traceback]
    J --> G
Loading

Reviews (3): Last reviewed commit: "Merge remote-tracking branch 'upstream/m..." | Re-trigger Greptile

Comment thread tests/test_backup.py Outdated
@bitflicker64

Copy link
Copy Markdown
Owner

/rewrite

@git-miku

git-miku Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

miku /rewrite

✅ PR description rewritten! Check the PR body above.

Preview

Summary

This PR fixes a silent failure in backup_db()'s rotation block where except Exception: pass swallowed all errors without logging, potentially causing unbounded backup accumulation when disk-full, permission errors, or filesystem issues occurred. The change narrows the exception handler to OSError (which covers PermissionError and FileNotFoundError) and adds logger.exception() to surface failures while preserving the guarantee that rotation errors never crash the backup process.

Changes

Core

  • termstory/backup.py: Added import logging and module-level logger = logging.getLogger(__name__) for error logging
  • termstory/backup.py: Changed rotation block exception handler from broad except Exception: to specific except OSError: to catch filesystem-related errors while allowing other exceptions to propagate
  • termstory/backup.py: Replaced bare pass with logger.exception("Failed to rotate old backups in %s", backup_dir) to log rotation failures with full traceback
  • termstory/backup.py: Updated comment to clarify that rotation failures must be visible in logs while still not crashing the backup process

Tests

  • tests/test_backup.py: Added test_backup_survives_rotation_failure() to verify that backup operations succeed even when rotation fails due to PermissionError
  • tests/test_backup.py: Test uses MockDatetime to create 11 backups (exceeding the 10-backup rotation threshold) and mocks os.remove to raise PermissionError, then asserts the backup still returns a valid file path

Fixes

Breaking Changes

None

Testing

Run the backup test suite to verify the fix:

python3 -m pytest tests/test_backup.py -v

The new test test_backup_survives_rotation_failure specifically validates that:

  1. Backups succeed even when rotation fails
  2. The backup path is still returned correctly
  3. Rotation errors are caught and logged without crashing the backup operation

Notes

The rotation logic maintains a 10-snapshot policy by deleting the oldest backups when the count exceeds 10 . This change ensures that filesystem errors during cleanup (such as disk-full conditions or permission issues) are now visible in logs while preserving the non-crashing contract that the backup operation itself always succeeds and returns a valid path.

@bitflicker64

Copy link
Copy Markdown
Owner

Can you go to the issue and do /claim, please?

@bitflicker64

Copy link
Copy Markdown
Owner

I'll triage it by the end of the day.

@bitflicker64 bitflicker64 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Approve with minor suggestions — production fix is correct and merge-safe; strengthen the test (and PR body) so logging can’t regress silently.

Intent

Fixes #296: rotation failures were swallowed with except Exception: pass and no logging. This PR narrows the catch to OSError, logs with logger.exception, and keeps the non-crashing backup contract.

Impact

Area Behavior
Happy path Unchanged
FS errors during rotation Still non-fatal; now logged at ERROR with traceback
Non-OSError bugs in the rotation block Previously swallowed; now propagate (intentional)
Return value Still returns backup_path when the SQLite backup succeeded

What’s correct

  • Tight scope: one production block + one focused regression test
  • Matches the issue’s suggested fix almost verbatim
  • Module-level logger = logging.getLogger(__name__) is standard and testable
  • Test correctly forces past the rotation threshold before monkeypatching os.remove
  • CI green across 3.9–3.12

Warnings

  1. Test never asserts logging — The core change is “log the failure.” The new test only checks that backup still returns a path. A silent revert to pass would still pass CI.
  2. PR description overclaims — Body says the test validates that errors are “caught and logged,” but logging is not asserted. Please align the description with what the test actually checks (or add the assertion below).

Suggestions

See inline comments. Summary:

  • Add a caplog assertion for logger.exception
  • Optionally assert backup count stays > 10 after the failing rotation
  • Nice-to-have: DRY MockDatetime; magic 10 → named constant is fine as a follow-up (as the issue already notes)

Not blocking

  • Keep logger.exception (not bare logger.error)
  • Keep OSError (do not re-widen to Exception)
  • Do not make rotation failures raise — callers rely on the non-crashing contract

Review posted from local review session

Comment thread tests/test_backup.py

# Rotation now fails, but the backup itself must still succeed and return a path.
backup_path = backup_db()
assert os.path.isfile(backup_path)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Assert that the failure is actually logged. Right now this only proves the backup still returns a path — a regression back to except OSError: pass would still pass.

Also, the PR body currently claims this test verifies errors are “caught and logged”; that claim is only true if we lock logging in here.

import logging

# ... setup / MockDatetime / 11 seed backups / monkeypatch os.remove ...

with caplog.at_level(logging.ERROR, logger="termstory.backup"):
    backup_path = backup_db()

assert os.path.isfile(backup_path)
assert any(
    "Failed to rotate old backups" in r.message
    for r in caplog.records
)

(Add caplog to the test fixture list: def test_backup_survives_rotation_failure(tmp_path, monkeypatch, caplog):.)

Comment thread tests/test_backup.py
raise PermissionError("cannot delete backup")

monkeypatch.setattr("termstory.backup.os.remove", boom)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional stronger assertion: After the failing rotation, also check that cleanup did not complete, so the test isn’t only checking that a new backup file exists:

from termstory.backup import _get_backup_dir
import glob

remaining = glob.glob(os.path.join(_get_backup_dir(), "termstory_backup_*.db"))
assert len(remaining) > 10  # rotation did not finish deleting

Nice-to-have, not blocking.

Comment thread termstory/backup.py
os.remove(oldest)
except Exception:
pass # Rotation failure should not crash the backup process
except OSError:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. OSError correctly covers PermissionError / FileNotFoundError / common FS failures, and logger.exception surfaces a traceback without failing the backup.

One intentional behavior change to be aware of: non-OSError exceptions in this block (e.g. unexpected bugs) now propagate instead of being swallowed. That’s the right tradeoff — just noting it so reviewers don’t treat this as a pure observability-only diff.

@bitflicker64

Copy link
Copy Markdown
Owner

/triage

@git-miku git-miku Bot added triage/triaged This PR has been triaged by a maintainer and removed needs-triage Awaiting human triage — run /triage to mark as triaged labels Jul 18, 2026
@git-miku

git-miku Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

miku /triage

✅ Marked as triaged (triage/triaged applied, needs-triage removed).

Signed-off-by: Sai Asish Y <say.apm35@gmail.com>
@SAY-5

SAY-5 commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Good catch, added the caplog assertion so the test now fails if the rotation error stops being logged. Also aligned the PR description with what the test checks. Left the count-remains check out to keep the test focused, and kept OSError / logger.exception as-is. All three backup tests pass locally.

@bitflicker64

Copy link
Copy Markdown
Owner

:miku /check-star

@git-miku

git-miku Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

miku /check-star

@SAY-5 still hasn't starred this repo. Star it first, then run :miku /check-star again. ⭐

@bitflicker64 bitflicker64 added the do-not-merge/needs-star PR author has not starred this repo — run /check-star after starring label Jul 18, 2026
@bitflicker64

Copy link
Copy Markdown
Owner

:miku /check-star

@git-miku

git-miku Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

miku /check-star

@SAY-5 still hasn't starred this repo. Applied do-not-merge/needs-star.

Star this repo first, then run :miku /check-star again. ⭐

@bitflicker64

Copy link
Copy Markdown
Owner

:miku /check-star

@git-miku

git-miku Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

miku /check-star

@SAY-5 still hasn't starred this repo. Applied do-not-merge/needs-star.

Star this repo first, then run :miku /check-star again. ⭐

@bitflicker64

Copy link
Copy Markdown
Owner

u can star it till the end of ecsoc thing

@bitflicker64 bitflicker64 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolve conflicts, please.

…ion-logging

Signed-off-by: Sai Asish Y <say.apm35@gmail.com>

# Conflicts:
#	termstory/backup.py
@git-miku

git-miku Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

👋 Thanks for the PR, @SAY-5!

To get this PR reviewed, please ⭐ star this repo — it's free and helps others discover it. Once starred, run :miku /check-star on this PR and I'll start helping out.

A do-not-merge/needs-star label has been applied to block merge until then.

@SAY-5

SAY-5 commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on main and resolved the conflict in backup.py, keeping logger.exception as you asked. The rotation test already asserts the failure is logged (the caplog check for "Failed to rotate old backups"), so a silent revert to pass would fail it. All backup tests pass locally.

@bitflicker64

Copy link
Copy Markdown
Owner

:miku /review

@git-miku

git-miku Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

miku /review

This is a clean fix that addresses the silent failure issue properly. The change from logger.warning to logger.exception in termstory/backup.py is the right call - rotation failures that could lead to unbounded backup accumulation deserve ERROR-level visibility with a full traceback .

The new test test_backup_survives_rotation_failure is well-designed: it creates enough backups to trigger rotation, mocks os.remove to fail with PermissionError, and then verifies both that the backup still succeeds AND that the failure is logged via caplog . This dual assertion ensures a regression to except OSError: pass would fail the test, which is exactly the safety net you want here.

One small nit: the PR description mentions adding import logging and a module-level logger, but the diff shows the logger was already in use (the previous code had logger.warning). The actual change is just the exception handler upgrade, which is correct - just worth noting the description overstates the diff slightly.

Notes

The backup rotation logic maintains a 10-snapshot policy by deleting the oldest backups when the count exceeds 10 . This change ensures filesystem errors during cleanup are now visible in logs while preserving the non-crashing contract that the backup operation itself always succeeds and returns a valid path.

@bitflicker64
bitflicker64 merged commit d7cb9c5 into bitflicker64:main Jul 20, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/storage area/storage do-not-merge/needs-star PR author has not starred this repo — run /check-star after starring kind/tests kind/tests size/S PR changes ~38 lines triage/triaged This PR has been triaged by a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Easy] backup.py: backup rotation failures swallowed by bare except Exception: pass

2 participants