Skip to content

fix(cross-seed): defer completion search while checking#1588

Open
Audionut wants to merge 11 commits intodevelopfrom
fix/crossseed-progress-handling
Open

fix(cross-seed): defer completion search while checking#1588
Audionut wants to merge 11 commits intodevelopfrom
fix/crossseed-progress-handling

Conversation

@Audionut
Copy link
Contributor

@Audionut Audionut commented Mar 10, 2026

Summary

  • defer completion-triggered cross-seed searches while qBittorrent reports the torrent in a checking state
  • refresh the torrent from qBittorrent before running completion search, then continue only after checking finishes at 100%
  • add completion queue tests for checking completion, incomplete-after-checking, and timeout behavior

Rationale

Commit 5a3114be6902b40d5760fdbd0acefa5b61628a8b (fix(qbittorrent): stop reboot torrent_completed spam) changed qBittorrent completion detection to use CompletionOn > 0 instead of requiring progress plus a non-checking state. That made completion events arrive while some torrents were still in checkingResumeData, which then hit the older cross-seed progress < 1.0 guard and logged [CROSSSEED-COMPLETION] Failed to execute completion search.

This change keeps the existing completeness requirement for completion searches, but defers execution while qBittorrent is still checking so the completion path matches the newer event timing.

Because the fix now waits by polling qBittorrent, it also tolerates transient refresh failures and re-checks skip conditions/settings after checking completes so completion searches still use current torrent state.

Testing

  • go test ./internal/services/crossseed ./internal/qbittorrent

Summary by CodeRabbit

  • New Features

    • Added configurable per-instance completion polling with readiness gating and retry/backoff controls to ensure torrents are fully ready before searches.
  • Bug Fixes

    • Cross-seed completion now reliably waits for ready states, with improved skip/timeout handling and clearer logging for deferred or skipped attempts.
  • Tests

    • Added extensive tests covering polling, deduplication, ordering, timeouts, state transitions, rate-limit retries, and deferred processing.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 10, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 836d02a1-7891-4c98-a067-e532840a84e9

📥 Commits

Reviewing files that changed from the base of the PR and between 4660d44 and 638ef39.

📒 Files selected for processing (2)
  • internal/services/crossseed/service.go
  • internal/services/crossseed/service_completion_queue_test.go
✅ Files skipped from review due to trivial changes (1)
  • internal/services/crossseed/service.go

Walkthrough

Adds injectable per-service completion polling timings and a per-instance polling/queueing mechanism that gates cross-seed completion searches until torrents leave checking states or time out; includes readiness helpers, deduped wait lanes, and expanded tests with a QBittorrent polling mock.

Changes

Cohort / File(s) Summary
Completion Polling Logic
internal/services/crossseed/service.go
Adds per-service injectable timings (poll interval, timeout, retry/backoff, max attempts), new private types (completionLane, completionWaitState, completionWaitSnapshot), readiness polling/registration, wait-lane poller, deduplication of waiters, helpers (waitForCompletionTorrentReady, getCompletionTorrent, isCompletionCheckingState, skip/log helpers), and integrates polling into HandleTorrentCompletion flow.
Completion Testing & Mocking
internal/services/crossseed/service_completion_queue_test.go
Adds completionPollingSyncMock to simulate per-hash QBittorrent state sequences and delays, test helpers to override timings/retry policy, and many tests covering queue ordering, serialized polling, checking->uploading transitions, deduplicated waiters, backoff/retry/timeouts, and rate-limit behavior.
Imports / Test Support
internal/services/crossseed/... (tests)
Introduces internalqb usage in tests and several stubbed mock methods to satisfy cross-instance QBittorrent interactions for polling scenarios.

Sequence Diagram

sequenceDiagram
    participant Handler as "HandleTorrentCompletion"
    participant Waiter as "waitForCompletionTorrentReady"
    participant Lane as "completionLane (poller)"
    participant Getter as "getCompletionTorrent"
    participant QBit as "QBittorrent API"
    participant Search as "executeCompletionSearch"

    Handler->>Waiter: waitForCompletionTorrentReady(ctx, instanceID, torrent)
    Waiter->>Lane: register wait / ensure poller running
    loop Poll until ready or timeout
        Lane->>Getter: getCompletionTorrent(ctx, instanceID, hash)
        Getter->>QBit: GetTorrents(ctx, hash)
        QBit-->>Getter: torrent details
        Getter-->>Lane: torrent snapshot
        alt isCompletionCheckingState(snapshot)
            Lane->>Lane: wait (poll interval) and retry
        else ready or terminal state
            Lane-->>Waiter: ready torrent or error
        end
    end
    Handler->>Search: executeCompletionSearch(torrent)
    Search-->>Handler: completion results
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

bugfix, cross-seed, tests, backend

Suggested reviewers

  • s0up4200
  • Barcode-eng

Poem

🐇 I polled the torrents, patient and small,
Waiting for checking to finish its call.
When readiness at last gave that gentle thump,
Cross-seeds hopped forward with a joyful jump! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: deferring completion search while torrents are in a checking state, which is the core fix addressed throughout the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/crossseed-progress-handling

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/services/crossseed/service_completion_queue_test.go`:
- Around line 169-180: The test helper setCompletionCheckingTimings must stop
mutating package globals completionCheckingPollInterval and
completionCheckingTimeout; instead, add injectable duration fields (e.g.,
completionPollInterval and completionTimeout) to the Service or to the helper
used by waitForCompletionTorrentReady/HandleTorrentCompletion, wire those fields
through the Service constructor or helper call in tests, update
waitForCompletionTorrentReady and HandleTorrentCompletion to read durations from
the Service/helper instance rather than package-level variables, and update
tests to pass custom timings per-Service so no global state is changed or shared
between parallel tests.

In `@internal/services/crossseed/service.go`:
- Around line 1505-1566: In waitForCompletionTorrentReady, don't abort the whole
wait on a transient getCompletionTorrent error: inside the ticker.C case where
current, err = s.getCompletionTorrent(...) is called, replace the immediate
return on error with a logged warning (include instanceID, hash/name, and err)
and continue the loop so retries occur until completionCheckingTimeout; leave
the initial pre-loop getCompletionTorrent error handling unchanged but ensure
the retry-path does not return err on a single API hiccup.
- Around line 1467-1479: The code uses the stale event payload for skip checks
before calling waitForCompletionTorrentReady, so after
waitForCompletionTorrentReady returns you must re-run the same guard/filter
logic against the refreshed torrent to avoid searching on a torrent that
gained/removed the cross-seed tag or moved into an excluded category;
specifically, after waitForCompletionTorrentReady returns (readyTorrent) and
before calling executeCompletionSearchWithRetry, refresh/re-evaluate the skip
conditions (the same checks that previously decided to skip) using readyTorrent
(and/or by reloading any settings used in those checks) and return early if the
torrent no longer qualifies.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4d63b694-1376-483c-8875-1d2522c863b5

📥 Commits

Reviewing files that changed from the base of the PR and between 77eedd9 and 33a6c57.

📒 Files selected for processing (2)
  • internal/services/crossseed/service.go
  • internal/services/crossseed/service_completion_queue_test.go

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@internal/services/crossseed/service.go`:
- Around line 1593-1601: The loop that calls s.getCompletionTorrent and retries
on error should stop polling immediately when getCompletionTorrent returns
ErrTorrentNotFound; modify the error handling around the getCompletionTorrent
call (the block handling current, err = s.getCompletionTorrent(...)) to check
for errors.Is(err, ErrTorrentNotFound) and in that case stop the polling for
this instance (e.g., break out / return / continue the outer workflow instead of
continuing the retry loop), logging a clear message with instanceID,
eventTorrent.Hash and eventTorrent.Name; leave other error cases to continue
retrying until completionTimeout as before.
- Around line 1444-1448: The code currently holds lane.mu across
waitForCompletionTorrentReady, which can block other completions; move the call
to waitForCompletionTorrentReady(ctx, instanceID, torrent) to before acquiring
the per-instance lane lock from getCompletionLane(instanceID), then only lock
lane.mu for the final revalidation and search steps; specifically, call
readyTorrent, err := s.waitForCompletionTorrentReady(...) first, then lane :=
s.getCompletionLane(instanceID); lane.mu.Lock()/defer lane.mu.Unlock() and
perform the revalidation + search using readyTorrent and torrent, ensuring any
necessary state is re-read under the lock to avoid races.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 07f36feb-9e79-4ab3-8e6c-7cf85380f908

📥 Commits

Reviewing files that changed from the base of the PR and between 33a6c57 and 4660d44.

📒 Files selected for processing (2)
  • internal/services/crossseed/service.go
  • internal/services/crossseed/service_completion_queue_test.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant