@@ -553,6 +553,29 @@ def edit(file: Path):
553553 temporary .rename (file )
554554
555555
556+ @contextmanager
557+ def wait_for_lock (
558+ path : Path , timeout : float = 600 , poll_interval : float = 10
559+ ) -> Iterator [None ]:
560+ """Context manager to hold *path* as a lock file, waiting up to *timeout* seconds for it."""
561+ deadline = perf_counter () + timeout
562+ while True :
563+ try :
564+ lock = zc .lockfile .LockFile (path )
565+ break
566+ except zc .lockfile .LockError as err :
567+ if perf_counter () >= deadline :
568+ raise TimeoutError (
569+ f"Gave up waiting for lock { path .name } after { timeout } seconds"
570+ ) from err
571+ logging .info ("Waiting for lock %s..." , path .name )
572+ sleep (poll_interval )
573+ try :
574+ yield
575+ finally :
576+ lock .close ()
577+
578+
556579def setup_switchers (script_content : bytes , html_root : Path ) -> None :
557580 """Setup cross-links between CPython versions:
558581 - Cross-link various languages in a language switcher
@@ -663,6 +686,13 @@ def run(self, http: urllib3.PoolManager, force_build: bool) -> bool | None:
663686 )
664687 else :
665688 return None # skipped
689+ except TimeoutError as err :
690+ # Another builder held the publish lock for too long; the docs
691+ # were built fine and will be published on the next run.
692+ logging .error ("%s" , err )
693+ if sentry_sdk :
694+ sentry_sdk .capture_exception (err )
695+ return False
666696 except Exception as err :
667697 logging .exception ("Badly handled exception, human, please help." )
668698 if sentry_sdk :
@@ -826,50 +856,58 @@ def copy_build_to_webroot(self, http: urllib3.PoolManager) -> None:
826856 language_dir .chmod (0o775 )
827857 target = language_dir / self .build_meta .version
828858
829- target .mkdir (parents = True , exist_ok = True )
830- try :
831- target .chmod (0o775 )
832- except PermissionError as err :
833- logging .warning ("Can't change mod of %s: %s" , target , str (err ))
834- chgrp (target , group = self .group , recursive = True )
835-
836- changed = 0
837- if self .includes_html :
838- # Copy built HTML files to webroot (default /srv/docs.python.org)
839- changed += changed_files (self .checkout / "Doc" / "build" / "html" , target )
840- logging .info ("Copying HTML files to %s" , target )
841- chgrp (
842- self .checkout / "Doc" / "build" / "html/" ,
843- group = self .group ,
844- recursive = True ,
845- )
846- chmod_make_readable (self .checkout / "Doc" / "build" / "html" )
847- run ((
848- "rsync" ,
849- "-a" ,
850- "--delete-delay" ,
851- "--filter" ,
852- "P archives/" ,
853- str (self .checkout / "Doc" / "build" / "html" ) + "/" ,
854- target ,
855- ))
856-
857- dist_dir = self .checkout / "Doc" / "dist"
858- if dist_dir .is_dir ():
859- # Copy archive files to /archives/
860- logging .debug ("Copying dist files." )
861- chgrp (dist_dir , group = self .group , recursive = True )
862- chmod_make_readable (dist_dir )
863- archives_dir = target / "archives"
864- archives_dir .mkdir (parents = True , exist_ok = True )
865- archives_dir .chmod (
866- archives_dir .stat ().st_mode | stat .S_IROTH | stat .S_IXOTH
867- )
868- chgrp (archives_dir , group = self .group )
869- changed += 1
870- for dist_file in dist_dir .iterdir ():
871- shutil .copy2 (dist_file , archives_dir / dist_file .name )
859+ # Builds run concurrently but may publish the same language/version to
860+ # the same directory, so serialise publishes per target.
861+ # Contention is expected, but brief, so wait instead of dying.
862+ with wait_for_lock (
863+ HERE / f"publish-{ self .build_meta .language } -{ self .build_meta .version } .lock"
864+ ):
865+ target .mkdir (parents = True , exist_ok = True )
866+ try :
867+ target .chmod (0o775 )
868+ except PermissionError as err :
869+ logging .warning ("Can't change mod of %s: %s" , target , str (err ))
870+ chgrp (target , group = self .group , recursive = True )
871+
872+ changed = 0
873+ if self .includes_html :
874+ # Copy built HTML files to webroot (default /srv/docs.python.org)
875+ changed += changed_files (
876+ self .checkout / "Doc" / "build" / "html" , target
877+ )
878+ logging .info ("Copying HTML files to %s" , target )
879+ chgrp (
880+ self .checkout / "Doc" / "build" / "html/" ,
881+ group = self .group ,
882+ recursive = True ,
883+ )
884+ chmod_make_readable (self .checkout / "Doc" / "build" / "html" )
885+ run ((
886+ "rsync" ,
887+ "-a" ,
888+ "--delete-delay" ,
889+ "--filter" ,
890+ "P archives/" ,
891+ str (self .checkout / "Doc" / "build" / "html" ) + "/" ,
892+ target ,
893+ ))
894+
895+ dist_dir = self .checkout / "Doc" / "dist"
896+ if dist_dir .is_dir ():
897+ # Copy archive files to /archives/
898+ logging .debug ("Copying dist files." )
899+ chgrp (dist_dir , group = self .group , recursive = True )
900+ chmod_make_readable (dist_dir )
901+ archives_dir = target / "archives"
902+ archives_dir .mkdir (parents = True , exist_ok = True )
903+ archives_dir .chmod (
904+ archives_dir .stat ().st_mode | stat .S_IROTH | stat .S_IXOTH
905+ )
906+ chgrp (archives_dir , group = self .group )
872907 changed += 1
908+ for dist_file in dist_dir .iterdir ():
909+ shutil .copy2 (dist_file , archives_dir / dist_file .name )
910+ changed += 1
873911
874912 logging .info ("%s files changed" , changed )
875913 if changed and not self .skip_cache_invalidation :
0 commit comments