Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/7896.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reduced lock contention for distribution updates that leave `base_path` unchanged.
28 changes: 23 additions & 5 deletions pulpcore/app/viewsets/publication.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,8 +527,26 @@ def get_queryset(self):
return qs

def async_reserved_resources(self, instance):
"""Return resource that locks all Distributions."""
return [f"pdrn:{get_domain().pulp_id}:distributions"]
"""
Reserve the narrowest safe lock for async distribution operations.

Creates, deletes, and base_path changes still lock the domain-wide distributions resource

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe add a statement that "base_path overlap validation" is the main concern why this function even exists.

I'm wondering if we can safely assume that deleting a distribution will never violate base_path overlaps?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes, that is the main reason this logic exists, and I can make that clearer in the docstring.

I would still keep deletes on the broader lock. A delete does not create an overlap by itself, but it does release a base_path, so it is still part of the same domain-wide consistency concern as creates and moves.

because base_path overlap validation is domain scoped. Other updates only need to lock the
specific distribution instance.
"""
domain_distributions = f"pdrn:{get_domain().pulp_id}:distributions"
if instance is None:
return [domain_distributions]

if getattr(self, "action", "") == "destroy":
return [instance, domain_distributions]

request_data = getattr(getattr(self, "request", None), "data", {})
requested_base_path = request_data.get("base_path", instance.base_path)
Comment thread
mdellweg marked this conversation as resolved.
if requested_base_path == instance.base_path:
return [instance]

return [instance, domain_distributions]


class ListDistributionViewSet(BaseDistributionViewSet, mixins.ListModelMixin):
Expand Down Expand Up @@ -567,9 +585,9 @@ class DistributionViewSet(
LabelsMixin,
):
"""
Provides read and list methods and also provides asynchronous CUD methods to dispatch tasks
with reservation that lock all Distributions preventing race conditions during base_path
checking.
Provides read and list methods plus asynchronous CUD methods that reserve the narrowest safe
distribution locks, only taking the domain-wide lock when base_path overlap validation or
base_path release needs it.
"""


Expand Down
60 changes: 60 additions & 0 deletions pulpcore/tests/functional/api/using_plugin/test_distributions.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,66 @@ def test_distribution_base_path(
assert json.loads(exc.value.body)["base_path"] is not None


@pytest.mark.parallel
def test_distribution_update_task_reservations(
file_bindings,
monitor_task,
):
create_task = monitor_task(
file_bindings.DistributionsFileApi.create(
{"name": str(uuid4()), "base_path": str(uuid4())}
).task
)
assert any(
resource.endswith(":distributions") for resource in create_task.reserved_resources_record
)
distribution = file_bindings.DistributionsFileApi.read(create_task.created_resources[0])
assert distribution.prn not in create_task.reserved_resources_record

no_base_path_update_task = monitor_task(
file_bindings.DistributionsFileApi.partial_update(
distribution.pulp_href,
{"name": str(uuid4())},
).task
)
assert distribution.prn in no_base_path_update_task.reserved_resources_record
assert not any(
resource.endswith(":distributions")
for resource in no_base_path_update_task.reserved_resources_record
)

unchanged_base_path_update_task = monitor_task(
file_bindings.DistributionsFileApi.partial_update(
distribution.pulp_href,
{"name": str(uuid4()), "base_path": distribution.base_path},
).task
)
assert distribution.prn in unchanged_base_path_update_task.reserved_resources_record
assert not any(
resource.endswith(":distributions")
for resource in unchanged_base_path_update_task.reserved_resources_record
)

base_path_update_task = monitor_task(
file_bindings.DistributionsFileApi.partial_update(
distribution.pulp_href, {"base_path": str(uuid4())}
).task
)
assert distribution.prn in base_path_update_task.reserved_resources_record
assert any(
resource.endswith(":distributions")
for resource in base_path_update_task.reserved_resources_record
)

delete_task = monitor_task(
file_bindings.DistributionsFileApi.delete(distribution.pulp_href).task
)
assert distribution.prn in delete_task.reserved_resources_record
assert any(
resource.endswith(":distributions") for resource in delete_task.reserved_resources_record
)


@pytest.mark.parallel
def test_distribution_filtering(
file_bindings,
Expand Down
Loading