Repository navigation
Mark a product file's transcoded videos deleted along with the file - #8682
Conversation
A `ProductFile` is marked deleted from three places that each call a different method: the editor's file save and the bundle "remove file" action call `mark_deleted`, `DeleteProductFilesWorker` calls `delete!` ten minutes after a product is deleted, and `ProductBuild` calls `mark_deleted!` when a build replaces its file. None of them touched the file's `transcoded_videos`, so the HLS renditions of a removed video stayed in S3. An `after_save` on `ProductFile` now fires when `deleted_at` goes from nil to set, whichever path did it, and marks the alive `TranscodedVideo` rows deleted. Everything downstream already exists: `TranscodedVideo#mark_deleted!` clears the HLS flag unless another alive row uses the same key, and `RemoveDeletedFilesFromS3Job` lists the `/hls/` prefix of each row deleted one to three days ago, skips it if an alive duplicate still points at the same key, and deletes the objects. The only other writer of `TranscodedVideo.deleted_at` is `ExpireTranscodedVideosJob`, whose cutoff defaults to 100 years unless the `transcoded_videos_recentness_limit_in_months` Redis key is set, so nothing reclaimed these objects. The ladder is comparable in size to the source, so this is roughly the same number of bytes the existing cleanup already saves. No user-facing change: a deleted file cannot be streamed or downloaded either way. Renditions shared with another alive file (a duplicated product) are left alone, as the job already does for sources. Test results: - `bin/rspec spec/models/product_file_spec.rb spec/sidekiq/remove_deleted_files_from_s3_job_spec.rb` -> 202 examples, 15 failures, all three new examples passing. The same 15 fail on a stashed baseline (199 examples, 15 failures), i.e. pre-existing in this environment. - Failing-first: with `app/models/product_file.rb` reverted to `origin/main` and the specs kept, the new example fails on `expect(transcoded_video.reload).to be_deleted`. - `bundle exec rubocop` on the three changed files: no offenses. Based on yashranaway#6 Co-authored-by: Aditya Garud <[email protected]> Co-authored-by: DeepSeek V4.1 Flash <[email protected]>
[Medium impact] The change reaches deleted product videos and their scheduled HLS storage cleanup. The callback appears sound, but the two job examples must meet the repository’s testing requirement before merging. Findings
Fix with agent prompt### Issue 1
spec/sidekiq/remove_deleted_files_from_s3_job_spec.rb:55-58
Both new job examples create an already-deleted `TranscodedVideo`, so neither exercises the new `ProductFile` callback. They test unchanged cleanup code and would still pass if the fix were removed. This violates the contributing guide’s requirement that tests fail when the fix is reverted, which must be satisfied before merging.
Start with a live file and its transcoded video, delete the file, then advance time and check cleanup in both examples.
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!
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.SummaryThis PR marks a file’s live transcoded videos deleted when the file is first marked deleted.
|
| it "removes the HLS renditions of a recently deleted transcoded video" do | ||
| transcoded_video = create(:transcoded_video, deleted_at: 26.hours.ago) | ||
| hls_dir = transcoded_video.transcoded_video_key.delete_suffix("index.m3u8") | ||
| allow_any_instance_of(described_class).to receive(:gather_transcoded_video_keys).and_return(["#{hls_dir}index.m3u8", "#{hls_dir}index_480p.m3u8"]) |
There was a problem hiding this comment.
Both new job examples create an already-deleted TranscodedVideo, so neither exercises the new ProductFile callback. They test unchanged cleanup code and would still pass if the fix were removed. This violates the contributing guide’s requirement that tests fail when the fix is reverted, which must be satisfied before merging.
Start with a live file and its transcoded video, delete the file, then advance time and check cleanup in both examples.
Context Used: AGENTS.md (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: spec/sidekiq/remove_deleted_files_from_s3_job_spec.rb
Line: 55-58
Comment:
**Tests bypass the fix**
Both new job examples create an already-deleted `TranscodedVideo`, so neither exercises the new `ProductFile` callback. They test unchanged cleanup code and would still pass if the fix were removed. This violates the contributing guide’s requirement that tests fail when the fix is reverted, which must be satisfied before merging.
Start with a live file and its transcoded video, delete the file, then advance time and check cleanup in both examples.
**Context Used:** AGENTS.md ([source](https://github.com/antiwork/gumroad/blob/main/AGENTS.md))
---
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!
There was a problem hiding this comment.
Fixed in 0564755. Both examples now start from a live file and its transcoded video, delete the file, advance 26 hours and assert on the keys the job deletes. With app/models/product_file.rb reverted to main exactly those two examples fail; with the change in, the file is 7 examples, 0 failures.
Both job examples built an already-deleted TranscodedVideo, so they passed with the model change reverted. Each now starts from a live file and video, deletes the file, advances 26 hours and asserts on the keys the job deletes.
What
A
ProductFilenow marks its transcoded videos deleted whenever the file itself is marked deleted, so the nightlyRemoveDeletedFilesFromS3Jobremoves the HLS renditions along with the source object.A
ProductFileis marked deleted from three places that each call a different method: the editor's file save and the bundle "remove file" action callmark_deleted,DeleteProductFilesWorkercallsdelete!ten minutes after a product is deleted, andProductBuildcallsmark_deleted!when a build replaces its file. None of them touched the file'stranscoded_videos. The newafter_saveonProductFilefires whendeleted_atgoes from nil to set, whichever path did it, and marks the alive rows deleted. Everything downstream already exists:TranscodedVideo#mark_deleted!clears the HLS flag unless another alive row uses the same key, and the S3 job lists the/hls/prefix of each row deleted one to three days ago, skips it if an alive duplicate still points at the same key, and deletes the objects.Why
When a seller removes a video, or deletes a product with videos, the source file is removed from S3 within a day or so, but its HLS ladder (a 480p rendition, plus 720p or 1080p for HD sources, as segmented
.tsfiles and playlists) stays in S3 Standard forever. The only other thing that marks aTranscodedVideodeleted isExpireTranscodedVideosJob, and its default cutoff is 100 years unless the Redis key is set, so nothing reclaims these objects.Every video deleted since HLS streaming started has left its renditions behind. The ladder is comparable in size to the source, so this is roughly the same number of bytes the existing cleanup already saves, just never collected. The backlog itself cannot be sized from outside the app; the console snippet in the source PR gives the count and the size, and the fix is forward-looking. To collect what is already orphaned, mark the existing rows on a console and the nightly job picks them up the next day.
Before / after
No user-facing change. Deleted files cannot be streamed or downloaded either way.
RemoveDeletedFilesFromS3Jobremoves the renditions alongside the source. Renditions shared with another alive file (a duplicated product) are left alone, as the job already does for sources.Backend behaviour only; nothing changes visually, so there is no screenshot or walkthrough media for this diff.
QA steps
Upload a video to a product and wait for it to transcode, then remove the file in the editor and save. In a console,
product_file.transcoded_videos.aliveis empty andproduct_file.is_transcoded_for_hls?is false. RunRemoveDeletedFilesFromS3Job.perform_async(3)aftertranscoded_video.update!(deleted_at: 26.hours.ago)and the/hls/prefix is gone from the bucket.Test results
bin/rspec spec/sidekiq/remove_deleted_files_from_s3_job_spec.rb-> 7 examples, 0 failures.bin/rspec spec/models/product_file_spec.rb -e "marking a file deleted"-> 1 example, 0 failures. The whole file carries 15 failures in this environment on a stashed baseline too (199 examples), pre-existing and unrelated to this diff.app/models/product_file.rbreverted toorigin/mainand the specs kept, all three new examples fail — the two job examples on the HLS keys never reachingdelete_s3_objects!, the model example onexpect(transcoded_video.reload).to be_deleted.bundle exec rubocopon the three changed files: no offenses.Based on yashranaway/gumroad#6 by @yashranaway.
AI Disclosure: the original PR was generated with Claude Code; reviewed, verified and imported here by Hermes Agent.
Premerge review: clean @ 0564755
Marketing: exempt (backend S3 lifecycle cleanup, no user-visible change) @ 0564755