Skip to content

Commit 9837ef2

Browse files
committed
Fix cache and DistributedPublication for repository_version distributions
Distributions serving via `repository_version` were missed by cache invalidation and DistributedPublication tracking when publications were created or deleted. Also fixes DistributedPublication to reactivate (clear `expires_at`) when switching back to a recently-superseded publication, instead of creating a duplicate record. closes #7993 Assisted-By: Claude Opus 4.6
1 parent 328dc6f commit 9837ef2

3 files changed

Lines changed: 427 additions & 13 deletions

File tree

CHANGES/7993.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed cache invalidation and DistributedPublication tracking for distributions serving via `repository_version` when publications are created or deleted.

pulpcore/app/models/publication.py

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,20 @@ def delete(self, **kwargs):
177177
except Publication.DoesNotExist:
178178
pass
179179

180+
# A distribution serving this publication's version directly (repository_version)
181+
# resolves to the latest publication of that version. Invalidate those distributions
182+
# when this is that latest publication.
183+
try:
184+
version_latest = Publication.objects.filter(
185+
repository_version=self.repository_version, complete=True
186+
).latest("pulp_created")
187+
if self.pk == version_latest.pk:
188+
base_paths |= Distribution.objects.filter(
189+
repository_version=self.repository_version
190+
).values_list("base_path", flat=True)
191+
except Publication.DoesNotExist:
192+
pass
193+
180194
# Invalidate cache for all distributions serving this publication
181195
if base_paths:
182196
Cache().delete(base_key=cache_key(base_paths))
@@ -234,17 +248,16 @@ def __exit__(self, exc_type, exc_val, exc_tb):
234248
self.delete()
235249
raise
236250

237-
# Create distributed publication for repository auto-publish scenario
251+
# Refresh distributed publications for the auto-publish scenario. A distribution can
252+
# distribute this publication indirectly either through its repository (latest
253+
# publication of the latest version) or through its repository_version (latest
254+
# publication of that version), without the distribution itself changing.
238255
if retain_distributed_pub_enabled():
239-
for distro in Distribution.objects.filter(repository=self.repository):
240-
detail_distro = distro.cast()
241-
if not detail_distro.SERVE_FROM_PUBLICATION:
242-
continue
243-
_, _, latest_repo_publication = (
244-
detail_distro.get_repository_publication_and_version()
245-
)
246-
if self == latest_repo_publication:
247-
DistributedPublication(distribution=distro, publication=self).save()
256+
for distro in Distribution.objects.filter(
257+
models.Q(repository=self.repository_version.repository)
258+
| models.Q(repository_version=self.repository_version)
259+
):
260+
distro.set_distributed_publication()
248261

249262
# Unmark old checkpoints if retention is configured
250263
if self.checkpoint:
@@ -254,7 +267,8 @@ def __exit__(self, exc_type, exc_val, exc_tb):
254267
# invalidate cache
255268
if settings.CACHE_ENABLED:
256269
base_paths = Distribution.objects.filter(
257-
repository=self.repository_version.repository
270+
models.Q(repository=self.repository_version.repository)
271+
| models.Q(repository_version=self.repository_version)
258272
).values_list("base_path", flat=True)
259273
if base_paths:
260274
Cache().delete(base_key=cache_key(base_paths))
@@ -800,14 +814,46 @@ def get_fallback_ca(self, path):
800814
is_not=None,
801815
)
802816
def set_distributed_publication(self):
803-
"""Track the publication being served when a distribution is created or changed."""
817+
"""
818+
Track the publication currently served by this distribution.
819+
820+
Records a DistributedPublication for the publication this distribution resolves to
821+
(directly, or indirectly via its repository/repository_version). Idempotent: does
822+
nothing if the active DistributedPublication already points at that publication.
823+
"""
804824
detail = self.cast()
805825
if not detail.SERVE_FROM_PUBLICATION or not retain_distributed_pub_enabled():
806826
return
807827
_, _, pub = detail.get_repository_publication_and_version()
808828
if pub is None:
809829
return
810-
DistributedPublication(distribution=self, publication=pub).save()
830+
831+
# Fast path: check if already active without locking (handles most calls)
832+
if DistributedPublication.objects.filter(
833+
distribution=self, publication=pub, expires_at__isnull=True
834+
).exists():
835+
return
836+
837+
# Slow path: need to create or reactivate under lock
838+
with transaction.atomic():
839+
# Lock the DP for this (distribution, publication) pair if it exists
840+
# At most one row exists per pair (either active or expiring, not both)
841+
dp = (
842+
DistributedPublication.objects.filter(distribution=self, publication=pub)
843+
.select_for_update()
844+
.first()
845+
)
846+
847+
if dp is None:
848+
# No DP exists, create one
849+
DistributedPublication(distribution=self, publication=pub).save()
850+
elif dp.expires_at is None:
851+
# Already active (race between fast path and here)
852+
return
853+
else:
854+
# Expiring DP exists, reactivate by clearing expires_at
855+
dp.expires_at = None
856+
dp.save(skip_hooks=True)
811857

812858
@hook(
813859
AFTER_UPDATE,

0 commit comments

Comments
 (0)