Skip to content

Fixes #39211 - Defer capsule distribution refreshes after all syncs#11700

Open
pablomh wants to merge 1 commit into
Katello:masterfrom
pablomh:fixes/capsule-refresh-distribution-race
Open

Fixes #39211 - Defer capsule distribution refreshes after all syncs#11700
pablomh wants to merge 1 commit into
Katello:masterfrom
pablomh:fixes/capsule-refresh-distribution-race

Conversation

@pablomh

@pablomh pablomh commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

Problem

When SyncCapsule runs multiple repos concurrently, each repo's RefreshDistribution was planned inline inside GenerateMetadata. Two repos sharing the same Pulp3 distribution (same base_path/name) could race to create it. The loser fails with a uniqueness Pulp3Error that surfaces only during async task polling - too late for a service-level rescue to catch.

Observed under concurrent capsule sync stress tests across multiple content types (RPM, container). Tracked in SAT-31994.

Solution

Two changes working together:

1. Deferred distribution refresh — post-sync planning step

Move distribution refresh out of per-repo GenerateMetadata into a post-sync planning step in SyncCapsule. After all sync batches complete, RefreshDistribution is planned for each repo, batched and run concurrently (concurrence block).

This also gives consumers a more consistent view of the capsule: content becomes accessible for all repos together rather than repo-by-repo as each finishes syncing. This mirrors the approach Pulp upstream adopted for its own replication task in pulpcore 3.107.0 (issue #7333).

2. rescue_external_task in RefreshDistribution

If two RefreshDistribution actions still race (e.g. two repos sharing the same distribution path), the loser recovers: re-invokes refresh_distributions, which finds the existing distribution and issues a partial_update instead of a create. Dynflow re-polls the new task automatically via suspend_and_ping after the rescue returns without raising (confirmed against Dynflow 2.0.0 source).

Test plan

  • Existing capsule sync planning tests updated to assert RefreshDistribution at the sync level
  • New GenerateMetadataTest: verifies RefreshDistribution is no longer planned inline
  • New RefreshDistributionTest: rescue_external_task recovers on uniqueness conflict, re-raises unrelated errors
  • Run capsule sync stress test with concurrent repos to verify no uniqueness failures

Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a concurrent bulk distribution-refresh task to schedule distribution refreshes across multiple Pulp3 repositories.
  • Bug Fixes

    • Automatic retry for certain distribution-creation conflicts (uniqueness/overlap) during refresh.
    • Capsule sync now schedules Pulp3 distribution refreshes only for Pulp3-supported repositories.
    • Metadata generation no longer inlines distribution refreshes.
  • Tests

    • Expanded tests covering planning, bulk refresh behavior, and retry/recovery logic.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In RefreshDistribution#invoke_external_task you removed the local smart_proxy/repo lookups but still reference smart_proxy and don't use the new repo helper, which will raise at runtime; either restore the smart_proxy lookup and use the repo helper, or add a smart_proxy helper method consistent with repo.
  • The distribution_uniqueness_conflict? check relies on substring matching 'base_path' and 'unique' in the error message, which is brittle; consider checking more structured attributes on Pulp3Error (or at least tightening the pattern to the exact Pulp uniqueness payload you expect) to avoid misclassifying unrelated errors.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `RefreshDistribution#invoke_external_task` you removed the local `smart_proxy`/`repo` lookups but still reference `smart_proxy` and don't use the new `repo` helper, which will raise at runtime; either restore the `smart_proxy` lookup and use the `repo` helper, or add a `smart_proxy` helper method consistent with `repo`.
- The `distribution_uniqueness_conflict?` check relies on substring matching `'base_path'` and `'unique'` in the error message, which is brittle; consider checking more structured attributes on `Pulp3Error` (or at least tightening the pattern to the exact Pulp uniqueness payload you expect) to avoid misclassifying unrelated errors.

## Individual Comments

### Comment 1
<location path="test/actions/katello/capsule_content_test.rb" line_range="67" />
<code_context>
-        assert_equal capsule_content.smart_proxy.id, input[:smart_proxy_id]
-        assert_equal repo.id, input[:repository_id]
-      end
+      assert_tree_planned_steps(tree, Actions::Pulp3::CapsuleContent::RefreshAllDistributions)
     end

</code_context>
<issue_to_address>
**suggestion (testing):** Sync planning tests only assert that `RefreshAllDistributions` exists, not that it is wired with the correct repositories and proxy.

Previously, this test verified that `RefreshDistribution` was planned with the expected `smart_proxy_id` and `repository_id` per repo. After switching to `RefreshAllDistributions`, it now only checks that the step exists. Since `RefreshAllDistributions` takes both a proxy and a list of repositories, the test should also assert that it receives the expected repos (and ideally the proxy) rather than, for example, an empty or incorrect list. For example, you could use `assert_tree_planned_with` (or a similar helper) to inspect the action inputs, or add a dedicated test in `SyncTest` that validates the inputs to `RefreshAllDistributions`, including the multi-repo case, to preserve the guarantees we had before.

Suggested implementation:

```ruby
      assert_tree_planned_with(tree, Actions::Pulp3::CapsuleContent::RefreshAllDistributions) do |input|
        # Ensure the correct capsule/smart proxy is used
        assert_equal capsule_content.smart_proxy.id, input[:smart_proxy_id]

        # Ensure the expected repositories are passed to the action
        # If the test sets up multiple repos, replace `[repo]` with the correct
        # collection (e.g. `repos`).
        expected_repo_ids = [repo].flatten.map(&:id).sort
        actual_repo_ids   = Array(input[:repository_ids] || input[:repository_id]).flatten.map(&:id).sort rescue Array(input[:repository_ids] || input[:repository_id]).sort

        assert_equal expected_repo_ids, actual_repo_ids
      end
    end

```

1. Adjust the `expected_repo_ids` construction to match the actual test setup:
   - If the test uses a single repo variable (`repo`), leave `[repo].flatten.map(&:id)` as-is.
   - If the test uses multiple repos (e.g. `repos`), change `expected_repo_ids = [repo].flatten.map(&:id).sort` to `expected_repo_ids = repos.map(&:id).sort`.
2. Confirm the input key used by `RefreshAllDistributions`:
   - If the action passes repository IDs under a different key (e.g. `:repository_ids`, `:repository_uuids`, or `:repository_href_list`), replace `:repository_ids`/`:repository_id` with the correct key(s) and adjust how IDs are extracted.
3. If `assert_tree_planned_with` does not yield the raw input hash but a wrapper object, you may need to access the inputs via `input.input` or similar, depending on the existing helper implementation.
4. Optionally, to explicitly cover the multi-repo case, add a separate `it` block in this file that sets up multiple repos on the capsule, runs the planning step, and uses the same `assert_tree_planned_with` pattern to assert that the full set of repo IDs is passed to `RefreshAllDistributions`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

assert_equal capsule_content.smart_proxy.id, input[:smart_proxy_id]
assert_equal repo.id, input[:repository_id]
end
assert_tree_planned_steps(tree, Actions::Pulp3::CapsuleContent::RefreshAllDistributions)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Sync planning tests only assert that RefreshAllDistributions exists, not that it is wired with the correct repositories and proxy.

Previously, this test verified that RefreshDistribution was planned with the expected smart_proxy_id and repository_id per repo. After switching to RefreshAllDistributions, it now only checks that the step exists. Since RefreshAllDistributions takes both a proxy and a list of repositories, the test should also assert that it receives the expected repos (and ideally the proxy) rather than, for example, an empty or incorrect list. For example, you could use assert_tree_planned_with (or a similar helper) to inspect the action inputs, or add a dedicated test in SyncTest that validates the inputs to RefreshAllDistributions, including the multi-repo case, to preserve the guarantees we had before.

Suggested implementation:

      assert_tree_planned_with(tree, Actions::Pulp3::CapsuleContent::RefreshAllDistributions) do |input|
        # Ensure the correct capsule/smart proxy is used
        assert_equal capsule_content.smart_proxy.id, input[:smart_proxy_id]

        # Ensure the expected repositories are passed to the action
        # If the test sets up multiple repos, replace `[repo]` with the correct
        # collection (e.g. `repos`).
        expected_repo_ids = [repo].flatten.map(&:id).sort
        actual_repo_ids   = Array(input[:repository_ids] || input[:repository_id]).flatten.map(&:id).sort rescue Array(input[:repository_ids] || input[:repository_id]).sort

        assert_equal expected_repo_ids, actual_repo_ids
      end
    end
  1. Adjust the expected_repo_ids construction to match the actual test setup:
    • If the test uses a single repo variable (repo), leave [repo].flatten.map(&:id) as-is.
    • If the test uses multiple repos (e.g. repos), change expected_repo_ids = [repo].flatten.map(&:id).sort to expected_repo_ids = repos.map(&:id).sort.
  2. Confirm the input key used by RefreshAllDistributions:
    • If the action passes repository IDs under a different key (e.g. :repository_ids, :repository_uuids, or :repository_href_list), replace :repository_ids/:repository_id with the correct key(s) and adjust how IDs are extracted.
  3. If assert_tree_planned_with does not yield the raw input hash but a wrapper object, you may need to access the inputs via input.input or similar, depending on the existing helper implementation.
  4. Optionally, to explicitly cover the multi-repo case, add a separate it block in this file that sets up multiple repos on the capsule, runs the planning step, and uses the same assert_tree_planned_with pattern to assert that the full set of repo IDs is passed to RefreshAllDistributions.

@coderabbitai

coderabbitai Bot commented Apr 2, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

SyncCapsule now filters computed repos for Pulp 3 support and, if any, plans a new concurrent action to refresh distributions for those repos. Pulp3 metadata generation stops auto-planning distribution refreshes when repository publication is skipped. A new RefreshAllDistributions action was added, and RefreshDistribution gained retry-on-uniqueness-conflict logic and simplified inputs.

Changes

Cohort / File(s) Summary
Sync planning
app/lib/actions/katello/capsule_content/sync_capsule.rb
Filter repos to pulp3_repos via smart_proxy.pulp3_support? and conditionally plan Actions::Pulp3::CapsuleContent::RefreshAllDistributions with smart_proxy + pulp3_repos.
Pulp3 orchestration
app/lib/actions/pulp3/capsule_content/refresh_all_distributions.rb
New RefreshAllDistributions action: plans RefreshDistribution for each repository inside a concurrence block.
Per-repository refresh
app/lib/actions/pulp3/capsule_content/refresh_distribution.rb
Signature simplified to (repository, smart_proxy), uses memoized repo/smart_proxy helpers, removes per-invoke DB lookups, and adds rescue_external_task to retry on distribution uniqueness/overlap errors.
Metadata generation
app/lib/actions/pulp3/capsule_content/generate_metadata.rb
Removed unconditional inlining of RefreshDistribution planning; only runs plan_self when repository.repository_type.pulp3_skip_publication is false.
Tests — Sync behavior updates
test/actions/katello/capsule_content_test.rb
Extended assertions to expect or refute planning of RefreshAllDistributions across various pulp3/pulp2 repo scenarios.
Tests — New and expanded Pulp3 tests
test/actions/pulp3/capsule_content/refresh_all_distributions_test.rb
Added tests for RefreshAllDistributions, GenerateMetadata planning behavior, and RefreshDistribution retry/rescue logic.

Sequence Diagram(s)

sequenceDiagram
  participant SyncCapsule as SyncCapsule
  participant RefreshAll as RefreshAllDistributions
  participant RefreshDist as RefreshDistribution
  participant SmartProxy as SmartProxy/Backend

  SyncCapsule->>SyncCapsule: compute repos
  SyncCapsule->>SyncCapsule: filter pulp3_repos (smart_proxy.pulp3_support?)
  alt pulp3_repos not empty
    SyncCapsule->>RefreshAll: plan(smart_proxy, pulp3_repos)
    RefreshAll->>RefreshDist: plan each (repository, smart_proxy) concurrently
    RefreshDist->>SmartProxy: invoke_external_task -> backend.refresh_distributions
    SmartProxy-->>RefreshDist: task result or Pulp3Error
    alt uniqueness/overlap error
      RefreshDist->>RefreshDist: rescue_external_task -> retry invoke_external_task
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 clearly reflects the main change: deferring capsule distribution refreshes until after syncs complete.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

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

Inline comments:
In `@app/lib/actions/pulp3/capsule_content/refresh_distribution.rb`:
- Around line 35-39: Update the rescue predicate in refresh_distribution.rb to
also treat the "Overlaps with existing distribution" overlap error as the same
recoverable create-race as the unique-base_path case: modify the condition in
the rescue/if that currently only matches "This field must be unique" to accept
either "base_path":["This field must be unique."] or "base_path":["Overlaps with
existing distribution"] (keep behavior consistent with
Katello::Pulp3::Repository#refresh_distributions), and add a unit test
exercising the async failure path that returns the overlap payload so the
overlap variant is covered.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a02f4517-e32e-448c-9641-1d54c9372247

📥 Commits

Reviewing files that changed from the base of the PR and between cc28088 and 959f3931df7409e7d07c0e69f6a4d057957772ee.

📒 Files selected for processing (6)
  • app/lib/actions/katello/capsule_content/sync_capsule.rb
  • app/lib/actions/pulp3/capsule_content/generate_metadata.rb
  • app/lib/actions/pulp3/capsule_content/refresh_all_distributions.rb
  • app/lib/actions/pulp3/capsule_content/refresh_distribution.rb
  • test/actions/katello/capsule_content_test.rb
  • test/actions/pulp3/capsule_content/refresh_all_distributions_test.rb

Comment thread app/lib/actions/pulp3/capsule_content/refresh_distribution.rb
@pablomh

pablomh commented Apr 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review! Addressing the smart_proxy concern: this is not a NameError at runtime — smart_proxy is defined as an instance method in Pulp3::Abstract, the base class of every Pulp3 action (see app/lib/actions/pulp3/abstract.rb). It returns SmartProxy.unscoped.find(input[:smart_proxy_id]) and is inherited by RefreshDistribution. No local variable is needed.

@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.

🧹 Nitpick comments (1)
app/lib/actions/pulp3/capsule_content/refresh_distribution.rb (1)

35-40: Consider sharing this conflict matcher with the service layer.

This string-based predicate now needs to stay aligned with the other refresh_distributions conflict handling path. Extracting one shared helper/constant would reduce the chance of the two matchers drifting again when Pulp changes its error text.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/actions/pulp3/capsule_content/refresh_distribution.rb` around lines
35 - 40, Extract the string-matching logic out of
distribution_uniqueness_conflict? into a shared helper (either a constant regex
or a predicate method) that both this method and the service layer's
refresh_distributions path can call; for example, create a
Pulp3ErrorMatchers::DISTRIBUTION_UNIQUENESS_PATTERN or
Pulp3ErrorMatchers.distribution_uniqueness_conflict?(error) and replace the
inline message checks in distribution_uniqueness_conflict? and in the
refresh_distributions conflict handling with calls to that shared symbol so the
matching logic is maintained in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/lib/actions/pulp3/capsule_content/refresh_distribution.rb`:
- Around line 35-40: Extract the string-matching logic out of
distribution_uniqueness_conflict? into a shared helper (either a constant regex
or a predicate method) that both this method and the service layer's
refresh_distributions path can call; for example, create a
Pulp3ErrorMatchers::DISTRIBUTION_UNIQUENESS_PATTERN or
Pulp3ErrorMatchers.distribution_uniqueness_conflict?(error) and replace the
inline message checks in distribution_uniqueness_conflict? and in the
refresh_distributions conflict handling with calls to that shared symbol so the
matching logic is maintained in one place.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 558436cb-132e-4614-a317-a5d139f859c2

📥 Commits

Reviewing files that changed from the base of the PR and between 959f3931df7409e7d07c0e69f6a4d057957772ee and 70a5ffd46a704a7745fdf38a59d84e5acbba170b.

📒 Files selected for processing (3)
  • app/lib/actions/pulp3/capsule_content/refresh_distribution.rb
  • test/actions/katello/capsule_content_test.rb
  • test/actions/pulp3/capsule_content/refresh_all_distributions_test.rb
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/actions/katello/capsule_content_test.rb
  • test/actions/pulp3/capsule_content/refresh_all_distributions_test.rb

@pablomh pablomh force-pushed the fixes/capsule-refresh-distribution-race branch from 70a5ffd to 006dd1c Compare April 2, 2026 16:01
@pablomh pablomh changed the title Fixes #39205 - Refresh capsule distributions atomically after all syncs Fixes #39211 - Refresh capsule distributions atomically after all syncs Apr 2, 2026

@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

🧹 Nitpick comments (1)
test/actions/katello/capsule_content_test.rb (1)

67-71: Assert the new planning boundary, not just action presence.

These checks still pass if GenerateMetadata keeps planning RefreshDistribution inline, because they only prove both actions exist somewhere in the tree. Please tighten this to show the RefreshDistribution steps are children of RefreshAllDistributions, or explicitly refute them under GenerateMetadata.

Also applies to: 108-112, 126-130, 140-143, 171-175, 250-257

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/actions/katello/capsule_content_test.rb` around lines 67 - 71, The tests
currently only assert that RefreshAllDistributions and RefreshDistribution exist
somewhere in the plan; update them to assert the planning boundary: locate the
Actions::Pulp3::CapsuleContent::RefreshAllDistributions node (using
assert_tree_planned_with / assert_tree_planned_steps) and then assert that its
child steps include Actions::Pulp3::CapsuleContent::RefreshDistribution with the
expected inputs (smart_proxy_id and repository_id); alternatively, add an
explicit negative assertion on Actions::Pulp3::CapsuleContent::GenerateMetadata
to ensure it does NOT plan RefreshDistribution inline. Use the existing helpers
(assert_tree_planned_with, assert_tree_planned_steps) around the
RefreshAllDistributions and GenerateMetadata nodes to enforce parent/child
relationships rather than just presence.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/lib/actions/pulp3/capsule_content/refresh_distribution.rb`:
- Around line 18-23: The rescue_external_task path currently re-invokes
invoke_external_task unbounded when distribution_uniqueness_conflict?(error)
hits; persist a one-time retry flag on the instance (e.g. `@refreshed_once` or set
a field like retried_refresh_distribution) before assigning self.external_task =
invoke_external_task and on subsequent calls check that flag and call
super(error) instead; update rescue_external_task to set the flag when
scheduling the replacement refresh and to fall back to super on the second
conflict to avoid infinite replay.

---

Nitpick comments:
In `@test/actions/katello/capsule_content_test.rb`:
- Around line 67-71: The tests currently only assert that
RefreshAllDistributions and RefreshDistribution exist somewhere in the plan;
update them to assert the planning boundary: locate the
Actions::Pulp3::CapsuleContent::RefreshAllDistributions node (using
assert_tree_planned_with / assert_tree_planned_steps) and then assert that its
child steps include Actions::Pulp3::CapsuleContent::RefreshDistribution with the
expected inputs (smart_proxy_id and repository_id); alternatively, add an
explicit negative assertion on Actions::Pulp3::CapsuleContent::GenerateMetadata
to ensure it does NOT plan RefreshDistribution inline. Use the existing helpers
(assert_tree_planned_with, assert_tree_planned_steps) around the
RefreshAllDistributions and GenerateMetadata nodes to enforce parent/child
relationships rather than just presence.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d48d6c9d-b496-4bbe-aacd-3e58f9e06c52

📥 Commits

Reviewing files that changed from the base of the PR and between 70a5ffd46a704a7745fdf38a59d84e5acbba170b and 006dd1c91b8a141676fc20809658657a94a7a124.

📒 Files selected for processing (6)
  • app/lib/actions/katello/capsule_content/sync_capsule.rb
  • app/lib/actions/pulp3/capsule_content/generate_metadata.rb
  • app/lib/actions/pulp3/capsule_content/refresh_all_distributions.rb
  • app/lib/actions/pulp3/capsule_content/refresh_distribution.rb
  • test/actions/katello/capsule_content_test.rb
  • test/actions/pulp3/capsule_content/refresh_all_distributions_test.rb
✅ Files skipped from review due to trivial changes (1)
  • test/actions/pulp3/capsule_content/refresh_all_distributions_test.rb
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/lib/actions/pulp3/capsule_content/refresh_all_distributions.rb
  • app/lib/actions/katello/capsule_content/sync_capsule.rb
  • app/lib/actions/pulp3/capsule_content/generate_metadata.rb

Comment thread app/lib/actions/pulp3/capsule_content/refresh_distribution.rb
@pablomh pablomh force-pushed the fixes/capsule-refresh-distribution-race branch from 006dd1c to 3b31409 Compare April 2, 2026 16:40

@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.

🧹 Nitpick comments (1)
test/actions/pulp3/capsule_content/refresh_all_distributions_test.rb (1)

20-31: Assert the exact refresh fan-out here.

This only proves each expected repo appears at least once. It would still pass if RefreshAllDistributions duplicated a repo or scheduled extras, so please add an exact count/set assertion for the planned RefreshDistribution children.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/actions/pulp3/capsule_content/refresh_all_distributions_test.rb` around
lines 20 - 31, The test currently only verifies each expected repository appears
at least once; update it to assert exact fan-out by checking the planned
RefreshDistribution children count equals repos.size and that the set of
(repository_id, smart_proxy_id) pairs exactly matches repos.map { |r| [r.id,
proxy.id] }. Use the existing
plan_action_tree(::Actions::Pulp3::CapsuleContent::RefreshAllDistributions,
proxy, repos) result and inspect its children for instances of
::Actions::Pulp3::CapsuleContent::RefreshDistribution, replacing the loose
assert_tree_planned_with loop with assertions that (1) children.select { |c|
c.action == ::Actions::Pulp3::CapsuleContent::RefreshDistribution }.size ==
repos.size and (2) the set of child inputs.map { |i| [i[:repository_id],
i[:smart_proxy_id]] } equals the expected set.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/actions/pulp3/capsule_content/refresh_all_distributions_test.rb`:
- Around line 20-31: The test currently only verifies each expected repository
appears at least once; update it to assert exact fan-out by checking the planned
RefreshDistribution children count equals repos.size and that the set of
(repository_id, smart_proxy_id) pairs exactly matches repos.map { |r| [r.id,
proxy.id] }. Use the existing
plan_action_tree(::Actions::Pulp3::CapsuleContent::RefreshAllDistributions,
proxy, repos) result and inspect its children for instances of
::Actions::Pulp3::CapsuleContent::RefreshDistribution, replacing the loose
assert_tree_planned_with loop with assertions that (1) children.select { |c|
c.action == ::Actions::Pulp3::CapsuleContent::RefreshDistribution }.size ==
repos.size and (2) the set of child inputs.map { |i| [i[:repository_id],
i[:smart_proxy_id]] } equals the expected set.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8cb0d181-65fa-47c5-9fdd-c83f4351ca0b

📥 Commits

Reviewing files that changed from the base of the PR and between 006dd1c91b8a141676fc20809658657a94a7a124 and 3b31409c0ef3e61a70c58034189e88b385dfd591.

📒 Files selected for processing (6)
  • app/lib/actions/katello/capsule_content/sync_capsule.rb
  • app/lib/actions/pulp3/capsule_content/generate_metadata.rb
  • app/lib/actions/pulp3/capsule_content/refresh_all_distributions.rb
  • app/lib/actions/pulp3/capsule_content/refresh_distribution.rb
  • test/actions/katello/capsule_content_test.rb
  • test/actions/pulp3/capsule_content/refresh_all_distributions_test.rb
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/lib/actions/pulp3/capsule_content/generate_metadata.rb
  • app/lib/actions/pulp3/capsule_content/refresh_all_distributions.rb
  • test/actions/katello/capsule_content_test.rb
  • app/lib/actions/katello/capsule_content/sync_capsule.rb

@pablomh pablomh force-pushed the fixes/capsule-refresh-distribution-race branch from 3b31409 to 558587b Compare April 2, 2026 17:01

@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.

🧹 Nitpick comments (2)
test/actions/pulp3/capsule_content/refresh_all_distributions_test.rb (2)

111-117: Assert error propagation for non-Pulp3Error exceptions.

Line 116 should also assert that the RuntimeError is re-raised (or failed) by parent handling; otherwise this test won’t catch future regressions that accidentally swallow non-Pulp errors.

Proposed test hardening
     it 'does not attempt recovery for non-Pulp3Errors' do
       action = create_action(::Actions::Pulp3::CapsuleContent::RefreshDistribution)
       action.stubs(:input).returns('repository_id' => repo.id, 'smart_proxy_id' => proxy.id)
       action.expects(:invoke_external_task).never

-      action.rescue_external_task(RuntimeError.new("connection refused"))
+      assert_raises(RuntimeError) do
+        action.rescue_external_task(RuntimeError.new("connection refused"))
+      end
     end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/actions/pulp3/capsule_content/refresh_all_distributions_test.rb` around
lines 111 - 117, The test currently stubs
action.rescue_external_task(RuntimeError.new(...)) but does not assert the
RuntimeError is re-raised; update the test for
Actions::Pulp3::CapsuleContent::RefreshDistribution (the action variable) to
wrap the rescue_external_task call in an assertion that the RuntimeError is
raised (e.g., use assert_raises or equivalent RSpec expectation) so the test
verifies non-Pulp3Error exceptions are propagated rather than swallowed.

57-63: Strengthen GenerateMetadata branch coverage.

Line 58 currently validates only “no inline RefreshDistribution”. Please add an explicit case for a repository type where pulp3_skip_publication is false to ensure the active planning branch is still exercised while keeping this assertion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/actions/pulp3/capsule_content/refresh_all_distributions_test.rb` around
lines 57 - 63, Add a new test case that uses a repository fixture where
pulp3_skip_publication is false (or mutate the repo fixture in the test) and
call plan_action_tree(::Actions::Pulp3::CapsuleContent::GenerateMetadata, repo,
proxy); keep the existing assertion refute_tree_planned(tree,
::Actions::Pulp3::CapsuleContent::RefreshDistribution) and also assert that the
active planning branch is exercised by asserting the plan includes
::Actions::Pulp3::CapsuleContent::RefreshAllDistributions (or that
GenerateMetadata triggers planning of RefreshAllDistributions) so the
GenerateMetadata branch for non-skipped publication is covered while still
ensuring no inline RefreshDistribution is planned.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/actions/pulp3/capsule_content/refresh_all_distributions_test.rb`:
- Around line 111-117: The test currently stubs
action.rescue_external_task(RuntimeError.new(...)) but does not assert the
RuntimeError is re-raised; update the test for
Actions::Pulp3::CapsuleContent::RefreshDistribution (the action variable) to
wrap the rescue_external_task call in an assertion that the RuntimeError is
raised (e.g., use assert_raises or equivalent RSpec expectation) so the test
verifies non-Pulp3Error exceptions are propagated rather than swallowed.
- Around line 57-63: Add a new test case that uses a repository fixture where
pulp3_skip_publication is false (or mutate the repo fixture in the test) and
call plan_action_tree(::Actions::Pulp3::CapsuleContent::GenerateMetadata, repo,
proxy); keep the existing assertion refute_tree_planned(tree,
::Actions::Pulp3::CapsuleContent::RefreshDistribution) and also assert that the
active planning branch is exercised by asserting the plan includes
::Actions::Pulp3::CapsuleContent::RefreshAllDistributions (or that
GenerateMetadata triggers planning of RefreshAllDistributions) so the
GenerateMetadata branch for non-skipped publication is covered while still
ensuring no inline RefreshDistribution is planned.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d69122aa-7e4c-4bd1-852c-49763d12f043

📥 Commits

Reviewing files that changed from the base of the PR and between 3b31409c0ef3e61a70c58034189e88b385dfd591 and 558587b54e258ae20fb52de855e196edda4525b3.

📒 Files selected for processing (6)
  • app/lib/actions/katello/capsule_content/sync_capsule.rb
  • app/lib/actions/pulp3/capsule_content/generate_metadata.rb
  • app/lib/actions/pulp3/capsule_content/refresh_all_distributions.rb
  • app/lib/actions/pulp3/capsule_content/refresh_distribution.rb
  • test/actions/katello/capsule_content_test.rb
  • test/actions/pulp3/capsule_content/refresh_all_distributions_test.rb
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/lib/actions/katello/capsule_content/sync_capsule.rb
  • app/lib/actions/pulp3/capsule_content/refresh_all_distributions.rb
  • app/lib/actions/pulp3/capsule_content/refresh_distribution.rb
  • test/actions/katello/capsule_content_test.rb

@pablomh pablomh force-pushed the fixes/capsule-refresh-distribution-race branch from 558587b to e02806d Compare April 2, 2026 17:08
@pablomh

pablomh commented Apr 2, 2026

Copy link
Copy Markdown
Contributor Author

Regarding nitpick 1 (assert_raises(RuntimeError)): this would make the test fail. For non-Pulp3Error exceptions, AbstractAsyncTask#rescue_external_task calls super which reaches Dynflow's base rescue_external_task. In the unit test context (action created via create_action), Dynflow's base handler silently retries when poll_attempts[:failed] < poll_max_retries — it does not re-raise. The test correctly asserts only that invoke_external_task is never called, which is the actual invariant we care about.

For nitpick 2: added a second GenerateMetadata test covering the pulp3_skip_publication: false branch (publication-based repos like file). Note: RefreshAllDistributions is planned by SyncCapsule, not by GenerateMetadata, so there is no RefreshAllDistributions assertion to add in the GenerateMetadataTest.

@pablomh pablomh force-pushed the fixes/capsule-refresh-distribution-race branch from e02806d to d257ab3 Compare April 2, 2026 20:13
@pablomh pablomh force-pushed the fixes/capsule-refresh-distribution-race branch from abcc5d7 to 5bfe7b8 Compare May 21, 2026 12:15
@pablomh pablomh force-pushed the fixes/capsule-refresh-distribution-race branch 2 times, most recently from 58ca8ec to 3451575 Compare June 4, 2026 13:35
@pablomh pablomh force-pushed the fixes/capsule-refresh-distribution-race branch from ae574af to 3919d3c Compare June 4, 2026 13:54
@pablomh pablomh force-pushed the fixes/capsule-refresh-distribution-race branch 3 times, most recently from 1fc68e9 to 7ed0b03 Compare June 4, 2026 17:38
@pablomh pablomh force-pushed the fixes/capsule-refresh-distribution-race branch from 7ed0b03 to abd97a0 Compare June 18, 2026 06:45
@pablomh pablomh force-pushed the fixes/capsule-refresh-distribution-race branch from abd97a0 to a1caee3 Compare June 30, 2026 10:26
@pablomh pablomh changed the title Fixes #39211 - Refresh capsule distributions atomically after all syncs Fixes #39211 - Defer capsule distribution refreshes after all syncs Jun 30, 2026
Comment on lines +4 to +10
CREATE_RACE_PATTERN = /
["']base_path["'].*?
(
must\ be\ unique | code=['"]unique |
Overlaps\ with\ existing\ distribution
)
/mx

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do you mind adding a comment on what this expression is doing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a comment — does this wording work?

# Matches Pulp3 errors indicating a distribution base_path conflict
# from a concurrent create: either a uniqueness violation or an overlap.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes, thank you

module Pulp3
module CapsuleContent
class RefreshDistribution < Pulp3::AbstractAsyncTask
include Helpers::Presenter

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why was this Presenter removed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It was unused — RefreshDistribution never defined a presenter method, so the include was a no-op. The sibling Repository::RefreshDistribution still includes it because it uses a custom presenter.

def sync_url_params(_sync_options)
params = {remote: repo.remote_href, mirror: repo.root.mirroring_policy == Katello::RootRepository::MIRRORING_POLICY_CONTENT}
params[:skip_types] = skip_types if (skip_types && repo.root.mirroring_policy != Katello::RootRepository::MIRRORING_POLICY_COMPLETE)
params[:skip_types] = skip_types if skip_types && repo.root.mirroring_policy != Katello::RootRepository::MIRRORING_POLICY_COMPLETE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is changing this intentional? I get its a style change but doesn't seem related to this issue so just want to make sure you meant to change this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Drive-by RuboCop style fix (redundant parentheses). Happy to revert if you'd prefer to keep it out of this PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't mind if it stays in this PR, just wanted to check.

Refresh capsule distributions after sync batches complete and recover
once from uniqueness races, while keeping the test coverage focused
on the deferred refresh flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@pablomh pablomh force-pushed the fixes/capsule-refresh-distribution-race branch from a1caee3 to 08910c6 Compare July 1, 2026 15:02

@aidenfine aidenfine left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just tested this manually and things seem to work fine. @ianballou will also be taking a look. LGTM

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