Fix cause of checksum failures in public binary mirror (#32407)

Move the copying of the buildcache to a root job that runs after all the child
pipelines have finished, so that the operation can be coordinated across all
child pipelines to remove the possibility of race conditions during potentially
simlutandous copies. This lets us ensure the .spec.json.sig and .spack files
for any spec in the root mirror always come from the same child pipeline
mirror (though which pipeline is arbitrary).  It also allows us to avoid copying
of duplicates, which we now do.
This commit is contained in:
Scott Wittenburg 2022-09-01 15:29:44 -06:00 committed by GitHub
parent d9313cf561
commit 6239198d65
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 119 additions and 49 deletions

View file

@ -36,6 +36,7 @@
import spack.util.executable as exe import spack.util.executable as exe
import spack.util.gpg as gpg_util import spack.util.gpg as gpg_util
import spack.util.spack_yaml as syaml import spack.util.spack_yaml as syaml
import spack.util.url as url_util
import spack.util.web as web_util import spack.util.web as web_util
from spack.error import SpackError from spack.error import SpackError
from spack.reporters.cdash import CDash from spack.reporters.cdash import CDash
@ -644,8 +645,6 @@ def generate_gitlab_ci_yaml(
# Values: "spack_pull_request", "spack_protected_branch", or not set # Values: "spack_pull_request", "spack_protected_branch", or not set
spack_pipeline_type = os.environ.get("SPACK_PIPELINE_TYPE", None) spack_pipeline_type = os.environ.get("SPACK_PIPELINE_TYPE", None)
spack_buildcache_copy = os.environ.get("SPACK_COPY_BUILDCACHE", None)
if "mirrors" not in yaml_root or len(yaml_root["mirrors"].values()) < 1: if "mirrors" not in yaml_root or len(yaml_root["mirrors"].values()) < 1:
tty.die("spack ci generate requires an env containing a mirror") tty.die("spack ci generate requires an env containing a mirror")
@ -653,6 +652,12 @@ def generate_gitlab_ci_yaml(
mirror_urls = [url for url in ci_mirrors.values()] mirror_urls = [url for url in ci_mirrors.values()]
remote_mirror_url = mirror_urls[0] remote_mirror_url = mirror_urls[0]
spack_buildcache_copy = os.environ.get("SPACK_COPY_BUILDCACHE", None)
if spack_buildcache_copy:
buildcache_copies = {}
buildcache_copy_src_prefix = remote_mirror_override or remote_mirror_url
buildcache_copy_dest_prefix = spack_buildcache_copy
# Check for a list of "known broken" specs that we should not bother # Check for a list of "known broken" specs that we should not bother
# trying to build. # trying to build.
broken_specs_url = "" broken_specs_url = ""
@ -1020,6 +1025,36 @@ def generate_gitlab_ci_yaml(
"{0} ({1})".format(release_spec, release_spec_dag_hash) "{0} ({1})".format(release_spec, release_spec_dag_hash)
) )
# Only keep track of these if we are copying rebuilt cache entries
if spack_buildcache_copy:
# TODO: This assumes signed version of the spec
buildcache_copies[release_spec_dag_hash] = [
{
"src": url_util.join(
buildcache_copy_src_prefix,
bindist.build_cache_relative_path(),
bindist.tarball_name(release_spec, ".spec.json.sig"),
),
"dest": url_util.join(
buildcache_copy_dest_prefix,
bindist.build_cache_relative_path(),
bindist.tarball_name(release_spec, ".spec.json.sig"),
),
},
{
"src": url_util.join(
buildcache_copy_src_prefix,
bindist.build_cache_relative_path(),
bindist.tarball_path_name(release_spec, ".spack"),
),
"dest": url_util.join(
buildcache_copy_dest_prefix,
bindist.build_cache_relative_path(),
bindist.tarball_path_name(release_spec, ".spack"),
),
},
]
if artifacts_root: if artifacts_root:
job_dependencies.append( job_dependencies.append(
{"job": generate_job_name, "pipeline": "{0}".format(parent_pipeline_id)} {"job": generate_job_name, "pipeline": "{0}".format(parent_pipeline_id)}
@ -1197,32 +1232,6 @@ def generate_gitlab_ci_yaml(
output_object["sign-pkgs"] = signing_job output_object["sign-pkgs"] = signing_job
if spack_buildcache_copy:
# Generate a job to copy the contents from wherever the builds are getting
# pushed to the url specified in the "SPACK_BUILDCACHE_COPY" environment
# variable.
src_url = remote_mirror_override or remote_mirror_url
dest_url = spack_buildcache_copy
stage_names.append("stage-copy-buildcache")
copy_job = {
"stage": "stage-copy-buildcache",
"tags": ["spack", "public", "medium", "aws", "x86_64"],
"image": "ghcr.io/spack/python-aws-bash:0.0.1",
"when": "on_success",
"interruptible": True,
"retry": service_job_retries,
"script": [
". ./share/spack/setup-env.sh",
"spack --version",
"aws s3 sync --exclude *index.json* --exclude *pgp* {0} {1}".format(
src_url, dest_url
),
],
}
output_object["copy-mirror"] = copy_job
if rebuild_index_enabled: if rebuild_index_enabled:
# Add a final job to regenerate the index # Add a final job to regenerate the index
stage_names.append("stage-rebuild-index") stage_names.append("stage-rebuild-index")
@ -1286,6 +1295,21 @@ def generate_gitlab_ci_yaml(
if spack_stack_name: if spack_stack_name:
output_object["variables"]["SPACK_CI_STACK_NAME"] = spack_stack_name output_object["variables"]["SPACK_CI_STACK_NAME"] = spack_stack_name
if spack_buildcache_copy:
# Write out the file describing specs that should be copied
copy_specs_dir = os.path.join(pipeline_artifacts_dir, "specs_to_copy")
if not os.path.exists(copy_specs_dir):
os.makedirs(copy_specs_dir)
copy_specs_file = os.path.join(
copy_specs_dir,
"copy_{}_specs.json".format(spack_stack_name if spack_stack_name else "rebuilt"),
)
with open(copy_specs_file, "w") as fd:
fd.write(json.dumps(buildcache_copies))
sorted_output = {} sorted_output = {}
for output_key, output_value in sorted(output_object.items()): for output_key, output_value in sorted(output_object.items()):
sorted_output[output_key] = output_value sorted_output[output_key] = output_value

View file

@ -2,6 +2,8 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details. # Spack Project Developers. See the top-level COPYRIGHT file for details.
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import glob
import json
import os import os
import shutil import shutil
import sys import sys
@ -271,7 +273,12 @@ def setup_parser(subparser):
# Sync buildcache entries from one mirror to another # Sync buildcache entries from one mirror to another
sync = subparsers.add_parser("sync", help=sync_fn.__doc__) sync = subparsers.add_parser("sync", help=sync_fn.__doc__)
source = sync.add_mutually_exclusive_group(required=True) sync.add_argument(
"--manifest-glob",
default=None,
help="A quoted glob pattern identifying copy manifest files",
)
source = sync.add_mutually_exclusive_group(required=False)
source.add_argument( source.add_argument(
"--src-directory", metavar="DIRECTORY", type=str, help="Source mirror as a local file path" "--src-directory", metavar="DIRECTORY", type=str, help="Source mirror as a local file path"
) )
@ -281,7 +288,7 @@ def setup_parser(subparser):
source.add_argument( source.add_argument(
"--src-mirror-url", metavar="MIRROR_URL", type=str, help="URL of the source mirror" "--src-mirror-url", metavar="MIRROR_URL", type=str, help="URL of the source mirror"
) )
dest = sync.add_mutually_exclusive_group(required=True) dest = sync.add_mutually_exclusive_group(required=False)
dest.add_argument( dest.add_argument(
"--dest-directory", "--dest-directory",
metavar="DIRECTORY", metavar="DIRECTORY",
@ -614,6 +621,31 @@ def copy_fn(args):
shutil.copyfile(specfile_src_path_yaml, specfile_dest_path_yaml) shutil.copyfile(specfile_src_path_yaml, specfile_dest_path_yaml)
def copy_buildcache_file(src_url, dest_url, local_path=None):
"""Copy from source url to destination url"""
tmpdir = None
if not local_path:
tmpdir = tempfile.mkdtemp()
local_path = os.path.join(tmpdir, os.path.basename(src_url))
try:
temp_stage = Stage(src_url, path=os.path.dirname(local_path))
try:
temp_stage.create()
temp_stage.fetch()
web_util.push_to_url(local_path, dest_url, keep_original=True)
except web_util.FetchError as e:
# Expected, since we have to try all the possible extensions
tty.debug("no such file: {0}".format(src_url))
tty.debug(e)
finally:
temp_stage.destroy()
finally:
if tmpdir and os.path.exists(tmpdir):
shutil.rmtree(tmpdir)
def sync_fn(args): def sync_fn(args):
"""Syncs binaries (and associated metadata) from one mirror to another. """Syncs binaries (and associated metadata) from one mirror to another.
Requires an active environment in order to know which specs to sync. Requires an active environment in order to know which specs to sync.
@ -622,6 +654,10 @@ def sync_fn(args):
src (str): Source mirror URL src (str): Source mirror URL
dest (str): Destination mirror URL dest (str): Destination mirror URL
""" """
if args.manifest_glob:
manifest_copy(glob.glob(args.manifest_glob))
return 0
# Figure out the source mirror # Figure out the source mirror
source_location = None source_location = None
if args.src_directory: if args.src_directory:
@ -687,8 +723,9 @@ def sync_fn(args):
buildcache_rel_paths.extend( buildcache_rel_paths.extend(
[ [
os.path.join(build_cache_dir, bindist.tarball_path_name(s, ".spack")), os.path.join(build_cache_dir, bindist.tarball_path_name(s, ".spack")),
os.path.join(build_cache_dir, bindist.tarball_name(s, ".spec.yaml")), os.path.join(build_cache_dir, bindist.tarball_name(s, ".spec.json.sig")),
os.path.join(build_cache_dir, bindist.tarball_name(s, ".spec.json")), os.path.join(build_cache_dir, bindist.tarball_name(s, ".spec.json")),
os.path.join(build_cache_dir, bindist.tarball_name(s, ".spec.yaml")),
] ]
) )
@ -701,24 +738,31 @@ def sync_fn(args):
dest_url = url_util.join(dest_mirror_url, rel_path) dest_url = url_util.join(dest_mirror_url, rel_path)
tty.debug("Copying {0} to {1} via {2}".format(src_url, dest_url, local_path)) tty.debug("Copying {0} to {1} via {2}".format(src_url, dest_url, local_path))
copy_buildcache_file(src_url, dest_url, local_path=local_path)
stage = Stage(
src_url, name="temporary_file", path=os.path.dirname(local_path), keep=True
)
try:
stage.create()
stage.fetch()
web_util.push_to_url(local_path, dest_url, keep_original=True)
except web_util.FetchError as e:
tty.debug("spack buildcache unable to sync {0}".format(rel_path))
tty.debug(e)
finally:
stage.destroy()
finally: finally:
shutil.rmtree(tmpdir) shutil.rmtree(tmpdir)
def manifest_copy(manifest_file_list):
"""Read manifest files containing information about specific specs to copy
from source to destination, remove duplicates since any binary packge for
a given hash should be the same as any other, and copy all files specified
in the manifest files."""
deduped_manifest = {}
for manifest_path in manifest_file_list:
with open(manifest_path) as fd:
manifest = json.loads(fd.read())
for spec_hash, copy_list in manifest.items():
# Last duplicate hash wins
deduped_manifest[spec_hash] = copy_list
for spec_hash, copy_list in deduped_manifest.items():
for copy_file in copy_list:
tty.debug("copying {0} to {1}".format(copy_file["src"], copy_file["dest"]))
copy_buildcache_file(copy_file["src"], copy_file["dest"])
def update_index(mirror_url, update_keys=False): def update_index(mirror_url, update_keys=False):
mirror = spack.mirror.MirrorCollection().lookup(mirror_url) mirror = spack.mirror.MirrorCollection().lookup(mirror_url)
outdir = url_util.format(mirror.push_url) outdir = url_util.format(mirror.push_url)

View file

@ -12,7 +12,7 @@ default:
- /^pr[\d]+_.*$/ - /^pr[\d]+_.*$/
- /^github\/pr[\d]+_.*$/ - /^github\/pr[\d]+_.*$/
variables: variables:
SPACK_BUILDCACHE_DESTINATION: "s3://spack-binaries-prs/${CI_COMMIT_REF_NAME}" SPACK_BUILDCACHE_DESTINATION: "s3://spack-binaries-prs/${CI_COMMIT_REF_NAME}/${SPACK_CI_STACK_NAME}"
SPACK_PIPELINE_TYPE: "spack_pull_request" SPACK_PIPELINE_TYPE: "spack_pull_request"
SPACK_PRUNE_UNTOUCHED: "True" SPACK_PRUNE_UNTOUCHED: "True"
@ -88,7 +88,7 @@ default:
protected-publish: protected-publish:
stage: publish stage: publish
extends: [ ".protected-refs" ] extends: [ ".protected" ]
image: "ghcr.io/spack/python-aws-bash:0.0.1" image: "ghcr.io/spack/python-aws-bash:0.0.1"
tags: ["spack", "public", "medium", "aws", "x86_64"] tags: ["spack", "public", "medium", "aws", "x86_64"]
variables: variables:
@ -97,7 +97,9 @@ protected-publish:
script: script:
- . "./share/spack/setup-env.sh" - . "./share/spack/setup-env.sh"
- spack --version - spack --version
- spack buildcache update-index --mirror-url "s3://spack-binaries/${CI_COMMIT_REF_NAME}" - export COPY_SPECS_DIR=${CI_PROJECT_DIR}/jobs_scratch_dir/specs_to_copy
- spack buildcache sync --manifest-glob "${COPY_SPECS_DIR}/*.json"
- spack buildcache update-index --mirror-url ${SPACK_COPY_BUILDCACHE}
######################################## ########################################
# TEMPLATE FOR ADDING ANOTHER PIPELINE # TEMPLATE FOR ADDING ANOTHER PIPELINE

View file

@ -569,7 +569,7 @@ _spack_buildcache_copy() {
} }
_spack_buildcache_sync() { _spack_buildcache_sync() {
SPACK_COMPREPLY="-h --help --src-directory --src-mirror-name --src-mirror-url --dest-directory --dest-mirror-name --dest-mirror-url" SPACK_COMPREPLY="-h --help --manifest-glob --src-directory --src-mirror-name --src-mirror-url --dest-directory --dest-mirror-name --dest-mirror-url"
} }
_spack_buildcache_update_index() { _spack_buildcache_update_index() {