Skip to content

fix(cl2k): stop concurrent Drive uploads creating duplicate files - #490

Merged
chodeus merged 5 commits into
developfrom
fix/cl2k-gdrive-upload-race
Aug 9, 2026
Merged

fix(cl2k): stop concurrent Drive uploads creating duplicate files#490
chodeus merged 5 commits into
developfrom
fix/cl2k-gdrive-upload-race

Conversation

@chodeus

@chodeus chodeus commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Summary

Saving the same artwork twice in quick succession left two files with identical names in the Drive folder instead of overwriting. Reproduced, fixed, and verified against the real Drive.

Related issue

N/A — reported from a live save (a logo filed twice into the Logos drive).

Type of change

  • Bug fix (non-breaking)
  • New feature (non-breaking)
  • Breaking change (config migration, schema, endpoint, or UI behavior)
  • Docs only
  • Refactor / internal cleanup

Root cause

Google Drive permits duplicate names in a folder — names are not unique keys. rclone decides create-vs-replace by listing the destination first, so two uploads overlapping in the same folder both see "absent" and both create.

_persist_poster defers the upload past the HTTP response (rclone outruns the UI timeout), so two quick saves overlap easily. Sequential uploads were always correct, which is exactly why this only happened sometimes.

Measured against the real Drive before the fix:

copies left
two uploads, sequential 1
two uploads, concurrent 2

The fix

Serialise writes per Drive folder. Both writers take the lock, not just the one that showed the symptom: the poster healer's move_file renames into the same namespace the maker uploads into, and a rename onto a name an upload is creating duplicates just as readily.

Reap after upload. Check whether the name just written now exists more than once, and collapse with rclone dedupe --dedupe-mode newest when it does — so the copy just uploaded is the survivor. This repairs duplicates the lock cannot prevent: ones already in the folder, or written by another instance. It costs one listing per upload and only runs a destructive command when a real duplicate exists, so the normal path never deletes anything.

A threading.Lock is sufficient because CHUB runs a single process (docker top shows one main.py). That assumption is stated at the lock — running multiple uvicorn workers would need a cross-process lock.

Testing

  • 7 tests driving upload_file concurrently against a fake that models the real failure shape: a listing, a window, then a create-or-replace.
  • One of them is a guard test that removes the lock and asserts the duplicate still reproduces. If the fake ever stops modelling the bug, that test fails rather than the real one silently passing for the wrong reason.
  • Also covered: per-folder locks don't serialise unrelated folders; a pre-existing duplicate is reaped keeping the newest; the normal path issues no destructive command; a failed duplicate check never fails the upload.
  • Verified end-to-end against the real Drive by staging the patched module into the container and re-running the same concurrent probe that produced the bug: 2 copies before, 1 after. The container was restored byte-identical afterwards (md5 confirmed).
  • ruff clean; 344 tests pass across the cl2k / gdrive / heal scope. Branch isolation stays additions-only.

Note on existing duplicates

The one duplicate already in the Logos drive was cleaned separately with rclone dedupe --dedupe-mode newest, keeping the most recent upload. All four configured Drive targets now report zero duplicated names.

Summary by CodeRabbit

  • Bug Fixes

    • Prevented concurrent uploads and renames from creating duplicate Google Drive files.
    • Added automatic cleanup of duplicate files after uploads.
    • Uploads now validate paths and credentials before starting.
    • Cleanup issues no longer cause successful uploads to fail.
    • Improved handling of filenames containing special characters.
  • Tests

    • Added coverage for concurrent uploads, duplicate handling, renames, validation, and failure scenarios.

Saving the same artwork twice in quick succession left two files with identical
names in the Drive folder instead of one. Google Drive permits duplicate names,
and rclone decides create-vs-replace by listing the destination first — so two
uploads overlapping in the same folder both see "absent" and both create. The
upload is deferred past the HTTP response (rclone outruns the UI timeout), so
two quick saves overlap easily. Sequential uploads were always fine, which is
why it only happened sometimes.

Serialise writes per Drive folder — both writers, since the poster healer
renames into the same namespace the maker uploads into. After each upload,
check whether the name just written now exists more than once and reap with
`rclone dedupe --dedupe-mode newest` when it does, so the fresh copy survives.
That also repairs duplicates the lock cannot prevent: ones already in the
folder, or written by another instance. The check costs one listing and only
runs a destructive command when a real duplicate exists.

Verified against the real Drive: the same concurrent probe that produced two
copies now produces one. The tests carry a guard that fails if the fake ever
stops reproducing the bug, so the fix cannot be silently neutered.
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fecc6c66-5e8b-40bc-8da7-b63a41a3bed4

📥 Commits

Reviewing files that changed from the base of the PR and between 45fd04d and 125c5c5.

📒 Files selected for processing (1)
  • tests/test_cl2k_gdrive_upload_race.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_cl2k_gdrive_upload_race.py

📝 Walkthrough

Walkthrough

The change adds per-folder locking for Drive uploads and renames, validates rclone-bound inputs, and performs best-effort duplicate cleanup after uploads. New tests cover race prevention, folder parallelism, cleanup behavior, rename coordination, and validation.

Changes

Drive upload consistency

Layer / File(s) Summary
Input validation and folder locking
backend/util/cl2k/gdrive_upload.py, tests/test_cl2k_gdrive_upload_race.py
The upload utility validates local paths and OAuth values before building rclone arguments. A per-folder lock registry synchronizes Drive mutations. Tests cover valid credentials and rejected option-shaped values.
Serialized uploads and duplicate cleanup
backend/util/cl2k/gdrive_upload.py, tests/test_cl2k_gdrive_upload_race.py
Uploads run under the folder lock. Post-upload cleanup lists folder contents, filters the uploaded filename, and retains the newest duplicate. Cleanup failures do not fail successful uploads.
Rename and upload coordination
backend/util/cl2k/gdrive_upload.py, tests/test_cl2k_gdrive_upload_race.py
Drive renames use the shared folder lock. Tests verify serialization within one folder and parallelism across different folders.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main fix: preventing duplicate Google Drive files from concurrent uploads.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cl2k-gdrive-upload-race

Comment @coderabbitai help to get the list of available commands.

Comment thread backend/util/cl2k/gdrive_upload.py Dismissed
CodeQL flags this call site as an uncontrolled command line (py/command-line-
injection). subprocess is list-form, so there is no shell and no shell
injection — the residual risk is argument injection: rclone reads a value
starting with "-" as an option rather than data.

folder_id and the service-account path already went through validators;
local_path and the three OAuth values did not. Validate them with the module's
existing _reject_unsafe, so the false-positive claim rests on the inputs
actually being checked rather than on the call being list-form alone.

Real Google credentials are unaffected — ids are numeric-led, secrets are
GOCSPX-…, the token is JSON — and that is asserted by a test alongside the
rejection cases. Verified against the live config and a real upload.
@chodeus

chodeus commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chodeus

chodeus commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/util/cl2k/gdrive_upload.py`:
- Around line 23-28: Shorten the comments in backend/util/cl2k/gdrive_upload.py
at lines 23-28, 109-112, 204-211, and 284-286 to 1–2 line navigational or
instructional notes: retain only the per-folder locking purpose and
multi-process limitation at 23-28, the option-shaped argument constraint at
109-112, the best-effort cleanup contract at 204-211, and that rename uses the
upload lock at 284-286; remove explanatory history and rationale while
preserving existing comment density.
- Around line 222-233: Update the rclone invocation in the upload flow around
subprocess.run to restrict deduplication to the target filename by adding the
appropriate name filter, or alternatively delete only that filename’s duplicate
object IDs. Add a fixture proving duplicates for a different filename remain
untouched, and require rclone version 1.61 or newer for this behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2f80f25b-acbb-4ed9-a0c3-f5d35fd5443c

📥 Commits

Reviewing files that changed from the base of the PR and between 26edbf8 and 0011c91.

📒 Files selected for processing (2)
  • backend/util/cl2k/gdrive_upload.py
  • tests/test_cl2k_gdrive_upload_race.py

Comment thread backend/util/cl2k/gdrive_upload.py Outdated
Comment thread backend/util/cl2k/gdrive_upload.py
The reap ran `rclone dedupe` across the whole folder, so uploading one file
could delete another file's duplicates — a destructive side effect outside the
operation's scope, triggered by an unrelated name. Filter it to the uploaded
filename (dedupe honours filters from rclone 1.61; the container runs 1.75).

The filter needs escaping: CL2K names carry `{tmdb-…}` and `{}` is alternation
in an rclone pattern, so an unescaped name matches the wrong thing or nothing.

Verified against the real Drive — staged duplicates of two names, reaped one,
and the other stayed at two copies. A unit test pins both the scope and the
escaping. Comment blocks trimmed to the repo's 1-2 line cap.
@chodeus

chodeus commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_cl2k_gdrive_upload_race.py`:
- Around line 226-240: The fake dedupe branch in fake_run must parse and apply
the command’s --include value instead of hard-coding "poster.png". Assert that
the filter exactly targets the uploaded filename, then remove only the matching
drive entry so the test fails for missing, incorrect, or broad dedupe filters
while preserving other.png duplicates.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6088ef56-04e5-4730-a227-0e750771415d

📥 Commits

Reviewing files that changed from the base of the PR and between 0011c91 and 45fd04d.

📒 Files selected for processing (2)
  • backend/util/cl2k/gdrive_upload.py
  • tests/test_cl2k_gdrive_upload_race.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/util/cl2k/gdrive_upload.py

Comment thread tests/test_cl2k_gdrive_upload_race.py
chodeus added 2 commits August 9, 2026 14:27
The fake removed a hard-coded name without reading --include, so a wrong or
absent filter still left the other file's duplicates intact and the test passed.
It proved nothing about the thing it existed to protect.

The fake now parses --include, unescapes it, and collapses only what it matches,
and the exact filter value is asserted. Both failure modes were confirmed to
fail the test: dropping the filter raises on the missing flag, and pointing it
at another file fails on '/other.png' == '/poster.png'.
@chodeus

chodeus commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chodeus
chodeus merged commit cc2b236 into develop Aug 9, 2026
12 checks passed
@chodeus
chodeus deleted the fix/cl2k-gdrive-upload-race branch August 9, 2026 07:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants