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