Skip to content

Commit 3c18032

Browse files
Serialise publishes to avoid concurrent-rsync failures (#342)
1 parent 0fa3e4d commit 3c18032

2 files changed

Lines changed: 84 additions & 44 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
/build_root/
22
/logs/
33
/www/
4-
# temporary lock file created while building the docs
4+
# temporary lock files created while building the docs
55
build_docs.lock
66
build_docs_archives.lock
77
build_docs_html.lock
8+
build_docs_html_en.lock
9+
publish-*.lock
810

911

1012
# Created by https://www.gitignore.io/api/python

build_docs.py

Lines changed: 81 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
556579
def 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

Comments
 (0)