Repository navigation
fix(conversion): stop touching converted copies on serve #5242
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,6 +48,10 @@ | |
| RETRY_AFTER_SECONDS: Final = 30 | ||
| PARTIAL_DIR: Final = ".partial" | ||
| FAILED_FILE: Final = ".failed" | ||
| # Records when a cached copy was last served. Its mtime (not the copy's) feeds | ||
| # cleanup: touching the copy itself would change nginx's ETag and break | ||
| # resumable downloads. | ||
| LAST_SERVED_FILE: Final = ".last_served" | ||
| # Give nginx time to open a copy after the X-Accel-Redirect response. | ||
| SERVE_GRACE_SECONDS: Final = 60 | ||
| # Room a conversion to an uncompressed target reserves per input byte; the | ||
|
|
@@ -140,16 +144,20 @@ def _lookup( | |
|
|
||
|
|
||
| def _serve_cached(final_path: Path, *, touch: bool) -> Path | None: | ||
| """`final_path` if it is cached, or None while cleanup or a conversion holds its dir.""" | ||
| """`final_path` if it is cached, or None while cleanup or a conversion holds its dir. | ||
|
|
||
| Args: | ||
| touch: Record the serve so cleanup spares the copy, without touching | ||
| the copy itself (its mtime feeds nginx's ETag). | ||
| """ | ||
| with ( | ||
| contextlib.suppress(OSError), | ||
| _try_lock(final_path.parent, shared=True) as locked, | ||
| ): | ||
| if locked and final_path.exists(): | ||
| if touch: | ||
| # Keep a served file fresh so TTL cleanup measures demand. | ||
| with contextlib.suppress(OSError): | ||
| os.utime(final_path) | ||
| (final_path.parent / LAST_SERVED_FILE).touch() | ||
| return final_path | ||
| return None | ||
|
|
||
|
|
@@ -165,7 +173,7 @@ def get_cached_converted( | |
| """The converted file if it is already cached, without converting. | ||
|
|
||
| Args: | ||
| touch: Refresh its mtime so TTL cleanup measures demand. | ||
| touch: Record the serve so cleanup spares the copy. | ||
| """ | ||
| found = _lookup(rom_id, rom_file, platform_slug, target) | ||
| return _serve_cached(found[1], touch=touch) if found else None | ||
|
|
@@ -242,7 +250,7 @@ async def resolve_format_download( | |
| Args: | ||
| allowed: The caller may start a conversion. | ||
| start: Start it now, rather than only report that one would run. | ||
| touch: Refresh a served copy's mtime so TTL cleanup measures demand. | ||
| touch: Record a served copy so cleanup spares it. | ||
| """ | ||
| if file_format(file.file_name) in formats: | ||
| return FormatResolution(FormatOutcome.ORIGINAL) | ||
|
|
@@ -311,8 +319,6 @@ async def get_or_convert( | |
| return None | ||
| # A conversion may have finished between the check above and the lock. | ||
| if final_path.exists(): | ||
| with contextlib.suppress(OSError): | ||
| os.utime(final_path) | ||
| return final_path | ||
| if (final_path.parent / FAILED_FILE).exists(): | ||
| return None | ||
|
|
@@ -388,20 +394,20 @@ def cleanup_stale_conversions(reserve_bytes: int = 0) -> int: | |
| now = time.time() | ||
| expired_before = now - converto.cache_ttl_hours * SECONDS_PER_HOUR | ||
| deleted = 0 | ||
| # (last served, size) of the final copies in each dir that survives the TTL pass. | ||
| # (newest mtime, size) of the final copies in each dir that survives the TTL pass. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Cleanup now uses conversion age, but Prompt To Fix With AIThis is a comment left during a code review.
Path: backend/utils/conversion_cache.py
Line: 381
Comment:
**Cache descriptions promise old behavior**
Cleanup now uses conversion age, but `cleanup_stale_conversions` still says it evicts the least recently served copies. The [conversion guide](https://github.com/rommapp/docs/blob/HEAD/docs/administration/library-conversion.md#the-conversion-cache) and [configuration reference](https://github.com/rommapp/docs/blob/HEAD/docs/reference/configuration-file.md#convertocache_ttl_hours) also describe expiry from the last serve and eviction by last use. Update these descriptions to conversion age so admins know that frequent downloads no longer keep a copy cached.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
| kept: list[tuple[float, int, Path]] = [] | ||
|
|
||
| for key_dir in cache_root.iterdir(): | ||
| if not key_dir.is_dir(): | ||
| continue | ||
| with contextlib.suppress(FileNotFoundError): | ||
| stats, last_served = _stat_files(key_dir) | ||
| stats, newest_mtime = _stat_files(key_dir) | ||
| partial_dir = key_dir / PARTIAL_DIR | ||
| if last_served < expired_before or partial_dir.exists(): | ||
| if newest_mtime < expired_before or partial_dir.exists(): | ||
| with _try_lock(key_dir) as locked: | ||
| if locked: | ||
| stats, last_served = _stat_files(key_dir) | ||
| if last_served < expired_before: | ||
| stats, newest_mtime = _stat_files(key_dir) | ||
| if newest_mtime < expired_before: | ||
| shutil.rmtree(key_dir, ignore_errors=True) | ||
| deleted += 1 | ||
| continue | ||
|
|
@@ -435,7 +441,7 @@ def cleanup_stale_conversions(reserve_bytes: int = 0) -> int: | |
| if used + reserve_bytes <= max_bytes: | ||
| break | ||
| with contextlib.suppress(FileNotFoundError), _try_lock(key_dir) as locked: | ||
| # A download may have touched the copy since the pass above. | ||
| # A conversion may have finished since the pass above. | ||
| if not locked or any( | ||
| p.stat().st_mtime >= now - SERVE_GRACE_SECONDS for p in _files(key_dir) | ||
| ): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The unit tests now expect unchanged mtime, but
test_get_refreshes_a_cached_copy_but_head_does_notstill requires GET to refresh it. Its fixture sets mtime one hour back, so all three GET cases fail at line 307 with the new serving behavior. Update that endpoint test's mtime and cleanup expectations too.Prompt To Fix With AI