Some checks failed
ci/crow/manual/backfill-flat-safety/5 Pipeline was successful
ci/crow/manual/backfill-flat-safety/3 Pipeline was successful
ci/crow/manual/backfill-flat-safety/17 Pipeline was successful
ci/crow/manual/backfill-flat-safety/13 Pipeline was successful
ci/crow/manual/backfill-flat-safety/15 Pipeline was successful
ci/crow/manual/backfill-flat-safety/7 Pipeline was successful
ci/crow/manual/backfill-flat-safety/1 Pipeline was successful
ci/crow/manual/backfill-flat-safety/9 Pipeline was successful
ci/crow/manual/backfill-flat-safety/11 Pipeline was successful
ci/crow/manual/backfill-flat-safety/8 Pipeline was successful
ci/crow/manual/backfill-flat-safety/12 Pipeline was successful
ci/crow/cron/process-updates/13 Pipeline failed
ci/crow/manual/backfill-flat-safety/2 Pipeline was successful
ci/crow/manual/backfill-flat-safety/10 Pipeline was successful
ci/crow/manual/backfill-flat-safety/14 Pipeline was successful
ci/crow/cron/process-updates/10 Pipeline was successful
ci/crow/cron/process-updates/14 Pipeline was successful
ci/crow/cron/process-updates/4 Pipeline was successful
ci/crow/cron/process-updates/15 Pipeline was successful
ci/crow/manual/backfill-flat-safety/18 Pipeline was successful
ci/crow/cron/process-updates/16 Pipeline was successful
ci/crow/manual/backfill-flat-safety/16 Pipeline was successful
ci/crow/cron/process-updates/11 Pipeline was successful
ci/crow/cron/process-updates/18 Pipeline was successful
ci/crow/cron/process-updates/12 Pipeline was successful
ci/crow/cron/process-updates/17 Pipeline was successful
ci/crow/manual/backfill-flat-safety/6 Pipeline was successful
ci/crow/manual/backfill-flat-safety/4 Pipeline was successful
ci/crow/cron/process-updates/6 Pipeline was successful
ci/crow/manual/repair-unsafe-flat-objects/13 Pipeline was successful
ci/crow/manual/repair-unsafe-flat-objects/15 Pipeline was successful
ci/crow/manual/repair-unsafe-flat-objects/9 Pipeline was successful
ci/crow/manual/repair-unsafe-flat-objects/10 Pipeline was successful
ci/crow/cron/process-updates/5 Pipeline was successful
ci/crow/manual/repair-unsafe-flat-objects/7 Pipeline was successful
ci/crow/manual/repair-unsafe-flat-objects/8 Pipeline was successful
ci/crow/manual/repair-unsafe-flat-objects/14 Pipeline was successful
ci/crow/manual/repair-unsafe-flat-objects/16 Pipeline was successful
ci/crow/manual/repair-unsafe-flat-objects/17 Pipeline was successful
ci/crow/manual/repair-unsafe-flat-objects/12 Pipeline was successful
ci/crow/manual/repair-unsafe-flat-objects/18 Pipeline was successful
ci/crow/manual/repair-unsafe-flat-objects/11 Pipeline was successful
ci/crow/manual/repair-unsafe-flat-objects/6 Pipeline was successful
ci/crow/manual/repair-unsafe-flat-objects/4 Pipeline was successful
ci/crow/manual/repair-unsafe-flat-objects/5 Pipeline was successful
ci/crow/manual/repair-unsafe-flat-objects/3 Pipeline was successful
ci/crow/manual/repair-unsafe-flat-objects/2 Pipeline was successful
ci/crow/cron/process-updates/3 Pipeline was successful
ci/crow/manual/repair-unsafe-flat-objects/1 Pipeline was successful
ci/crow/manual/reindex/13 Pipeline was successful
ci/crow/manual/reindex/7 Pipeline was successful
ci/crow/manual/reindex/11 Pipeline was successful
ci/crow/manual/reindex/15 Pipeline was successful
ci/crow/manual/reindex/17 Pipeline was successful
ci/crow/manual/reindex/12 Pipeline was successful
ci/crow/manual/reindex/9 Pipeline was successful
ci/crow/manual/reindex/3 Pipeline was successful
ci/crow/manual/reindex/14 Pipeline was successful
ci/crow/manual/reindex/1 Pipeline was successful
ci/crow/manual/reindex/2 Pipeline was successful
ci/crow/manual/reindex/4 Pipeline was successful
ci/crow/manual/reindex/6 Pipeline was successful
ci/crow/manual/reindex/5 Pipeline was successful
ci/crow/manual/reindex/16 Pipeline was successful
ci/crow/manual/reindex/18 Pipeline was successful
ci/crow/manual/reindex/8 Pipeline was successful
ci/crow/manual/reindex/10 Pipeline was successful
ci/crow/cron/process-updates/2 Pipeline was successful
ci/crow/cron/process-updates/7 Pipeline was successful
ci/crow/cron/process-updates/1 Pipeline was successful
ci/crow/manual/verify-repo-routing/13 Pipeline was successful
ci/crow/manual/verify-repo-routing/3 Pipeline was successful
ci/crow/manual/verify-repo-routing/1 Pipeline failed
ci/crow/manual/verify-repo-routing/15 Pipeline was successful
ci/crow/manual/verify-repo-routing/2 Pipeline failed
ci/crow/manual/verify-repo-routing/4 Pipeline was successful
ci/crow/manual/verify-repo-routing/17 Pipeline was successful
ci/crow/manual/verify-repo-routing/5 Pipeline was successful
ci/crow/manual/verify-repo-routing/6 Pipeline was successful
ci/crow/manual/verify-repo-routing/9 Pipeline was successful
ci/crow/manual/verify-repo-routing/11 Pipeline was successful
ci/crow/manual/verify-repo-routing/14 Pipeline was successful
ci/crow/manual/verify-repo-routing/16 Pipeline was successful
ci/crow/manual/verify-repo-routing/10 Pipeline was successful
ci/crow/manual/verify-repo-routing/12 Pipeline was successful
ci/crow/manual/verify-repo-routing/7 Pipeline was successful
ci/crow/manual/verify-repo-routing/8 Pipeline was successful
ci/crow/manual/verify-repo-routing/18 Pipeline was successful
ci/crow/cron/process-updates/9 Pipeline was successful
ci/crow/cron/process-updates/8 Pipeline was successful
## Why The backfill decided which flat objects were worth inspecting by looking the package up in **CRAN's current index**: ```r compiled_names <- cran[NeedsCompilation == "yes", "Package"] keep <- flat_pkg %in% compiled_names ``` CRAN lists only what it currently ships. A package CRAN has **archived** is absent from that index, so it is never recognised as compiled and never inspected, and no verdict is ever recorded for it. `union_index_records()` meanwhile reads the slot's *own* `NeedsCompilation`, finds `yes`, and drops the record because the source classifier flags the package and no safe verdict exists to clear it. The repair cannot rescue it either: it works from recorded verdicts, and there is no row. The result is unrecoverable, and identical in shape to #132 but from a different cause: dropped from every per-minor index, on every re-index, forever. ## Evidence `mixedMem` and `vcpen` survived a backfill, two re-indexes and a repair while missing from all three rhel8 per-minor indexes on **both** architectures: ``` rhel8 amd64 4.5: missing=3 [mixedMem timbr vcpen] rhel8 arm64 4.5: missing=2 [mixedMem vcpen] ``` Neither appears anywhere in either repair log, because neither was ever in the unsafe set. Their per-minor objects are 404 at every minor. And in CRAN's index: ``` mixedMem CRAN version=ABSENT NeedsCompilation=ABSENT vcpen CRAN version=ABSENT NeedsCompilation=ABSENT ``` Both removed from CRAN. Their flat records in our own index say `NeedsCompilation: yes`. ## What changed `NeedsCompilation` now comes from the slot's own flat index, which describes exactly the objects being inspected rather than a different repository's opinion of them. - Fetched **from the bucket**, not over HTTPS. This runs inside R, so the edge would route the request to a per-minor index and the flat objects would not all be there. Same reason the object listing above it comes from S3 (#203). - Keyed on `<package>_<version>`, not the package name: a slot can carry several versions and only some of them compile. - Refuses to proceed on an empty read, so an unreadable index cannot silently mean "nothing is compiled" and skip the entire slot. ## Effect Re-running the backfill on a slot will now inspect these objects and record verdicts, after which the index writer keeps the safe ones and the repair can act on the unsafe ones. Already-recorded objects are still skipped, so re-running stays cheap. Reviewed-on: #212
199 lines
6.4 KiB
R
199 lines
6.4 KiB
R
### Record, per flat-slot object, whether it loads under every supported R minor.
|
|
#
|
|
# Usage: Rscript local/backfill-flat-safety.R <arch> <codename> [limit]
|
|
#
|
|
# `union_index_records()` keeps a carried flat record when that object is
|
|
# recorded safe, and drops it otherwise. Objects uploaded before that record
|
|
# existed have no verdict, so they are dropped: a full re-index removed ~2000
|
|
# records per per-minor index, all resolving to CRAN source for clients the
|
|
# routing serves correctly.
|
|
#
|
|
# This inspects the objects already in the bucket and writes the verdict, which
|
|
# is what restores that coverage. The question is decided by symbols, not
|
|
# heuristics: a binary fails at dyn.load() under minor M exactly when it
|
|
# references a symbol M does not export, and the volatile set is derived by
|
|
# diffing the R builds installed in this image.
|
|
#
|
|
# Only compiled packages are inspected. A package with no shared object cannot
|
|
# fail this way and is never dropped, so proving it would be wasted downloads.
|
|
#
|
|
# Resumable: objects already recorded are skipped, so a re-run costs one query.
|
|
|
|
options(error = function() {
|
|
cat("ERROR:", geterrmessage(), "\n", file = stdout())
|
|
traceback(2)
|
|
q(status = 1)
|
|
})
|
|
|
|
library(bincraft, quietly = TRUE)
|
|
|
|
args <- commandArgs(trailingOnly = TRUE)
|
|
if (length(args) < 2L) {
|
|
stop("usage: backfill-flat-safety.R <arch> <codename> [limit]", call. = FALSE)
|
|
}
|
|
arch <- args[[1L]]
|
|
codename <- args[[2L]]
|
|
limit <- if (length(args) >= 3L) as.integer(args[[3L]]) else NA_integer_
|
|
|
|
sets <- bincraft:::installed_r_symbol_sets()
|
|
if (is.null(sets)) {
|
|
stop(
|
|
"Fewer than two R minors are installed here, so nothing can be compared. Refusing to record verdicts that would all read 'safe'.", # nolint
|
|
call. = FALSE
|
|
)
|
|
}
|
|
cat(sprintf(
|
|
"Comparing R minors: %s (%d symbols vary across them)\n",
|
|
paste(names(sets), collapse = ", "),
|
|
length(bincraft:::cross_minor_volatile_symbols(sets))
|
|
))
|
|
|
|
# The flat slot is listed from S3, not from PACKAGES.gz over HTTPS. Reading the
|
|
# index from inside R sends an R User-Agent, which the edge routes to a
|
|
# per-minor index; that enumerates union records carrying `Path: <minor>`, whose
|
|
# objects do not exist at the flat path. Every download then 404s: a first run
|
|
# skipped 292 of 300 that way. The bucket cannot be routed and is authoritative.
|
|
s3fs::s3_file_system(
|
|
aws_access_key_id = Sys.getenv("B2_S3_ACCESS_KEY"),
|
|
aws_secret_access_key = Sys.getenv("B2_S3_SECRET_KEY"),
|
|
endpoint = "https://s3.eu-central-003.backblazeb2.com",
|
|
region_name = "eu-central-003",
|
|
refresh = TRUE
|
|
)
|
|
contrib <- sprintf(
|
|
"devxy-rpkgs-binaries/%s/%s/latest/src/contrib",
|
|
arch,
|
|
codename
|
|
)
|
|
objects <- s3fs::s3_dir_ls(contrib, recurse = TRUE)
|
|
relative <- sub(sprintf("^(s3://)?%s/", contrib), "", objects)
|
|
# Flat objects only: no `<minor>/` prefix and nothing under Archive/.
|
|
flat_files <- relative[
|
|
grepl("\\.tar\\.gz$", relative) & !grepl("/", relative, fixed = TRUE)
|
|
]
|
|
flat_pkg <- sub("_.*$", "", flat_files)
|
|
flat_ver <- sub("\\.tar\\.gz$", "", sub("^[^_]*_", "", flat_files))
|
|
|
|
# Only compiled packages can fail at dyn.load(), so the rest are not worth
|
|
# downloading. Ask the slot's own index rather than CRAN's: CRAN lists only the
|
|
# versions it currently ships, so a package it has archived is absent, is never
|
|
# recognised as compiled, and is never inspected. No verdict is ever recorded
|
|
# for it, while `union_index_records()` reads the slot's own `NeedsCompilation`
|
|
# and happily drops it -- with nothing able to clear the flag, because the
|
|
# repair works from recorded verdicts too. `mixedMem` and `vcpen`, both removed
|
|
# from CRAN, were stuck out of every rhel8 per-minor index that way.
|
|
#
|
|
# Read it from the bucket, not over HTTPS: this runs inside R, so the edge would
|
|
# route the request to a per-minor index and the flat objects would not all be
|
|
# there. Same reason the object listing above comes from S3.
|
|
index_file <- file.path(tempdir(), "flat-PACKAGES.gz")
|
|
s3fs::s3_file_download(
|
|
sprintf("%s/PACKAGES.gz", contrib),
|
|
index_file,
|
|
overwrite = TRUE
|
|
)
|
|
index <- read.dcf(
|
|
gzfile(index_file),
|
|
fields = c("Package", "Version", "NeedsCompilation")
|
|
)
|
|
if (nrow(index) == 0L) {
|
|
stop(
|
|
sprintf(
|
|
"Read no records from %s/PACKAGES.gz; refusing to guess what is compiled.",
|
|
contrib
|
|
),
|
|
call. = FALSE
|
|
)
|
|
}
|
|
# Key on the object, not the package: a slot can carry several versions and
|
|
# only some of them compile.
|
|
compiled_objects <- paste(
|
|
index[, "Package"],
|
|
index[, "Version"],
|
|
sep = "_"
|
|
)[
|
|
!is.na(index[, "NeedsCompilation"]) &
|
|
tolower(index[, "NeedsCompilation"]) == "yes"
|
|
]
|
|
keep <- paste(flat_pkg, flat_ver, sep = "_") %in% compiled_objects
|
|
compiled <- data.frame(
|
|
Package = flat_pkg[keep],
|
|
Version = flat_ver[keep],
|
|
stringsAsFactors = FALSE
|
|
)
|
|
cat(sprintf(
|
|
"%s/%s: %d flat objects, %d compiled and worth inspecting\n",
|
|
arch,
|
|
codename,
|
|
length(flat_files),
|
|
nrow(compiled)
|
|
))
|
|
if (!is.na(limit) && limit > 0L && limit < nrow(compiled)) {
|
|
compiled <- compiled[seq_len(limit), , drop = FALSE]
|
|
cat(sprintf("Limited to the first %d\n", limit))
|
|
}
|
|
|
|
# Fetch from the bucket as well: the edge does not rewrite tarball requests,
|
|
# but it does cache them, and the object is what the verdict is about.
|
|
safe <- 0L
|
|
unsafe <- 0L
|
|
skipped <- 0L
|
|
for (i in seq_len(nrow(compiled))) {
|
|
pkg <- compiled[i, "Package"]
|
|
ver <- compiled[i, "Version"]
|
|
tarball <- file.path(tempdir(), sprintf("%s_%s.tar.gz", pkg, ver))
|
|
ok <- tryCatch(
|
|
{
|
|
s3fs::s3_file_download(
|
|
sprintf("%s/%s_%s.tar.gz", contrib, pkg, ver),
|
|
tarball,
|
|
overwrite = TRUE
|
|
)
|
|
file.exists(tarball)
|
|
},
|
|
error = function(e) FALSE,
|
|
warning = function(w) FALSE
|
|
)
|
|
if (!ok) {
|
|
skipped <- skipped + 1L
|
|
next
|
|
}
|
|
verdict <- bincraft:::tarball_abi_verdict(tarball, sets)
|
|
unlink(tarball)
|
|
if (!isTRUE(verdict$inspected)) {
|
|
skipped <- skipped + 1L
|
|
next
|
|
}
|
|
bincraft:::flat_safety_store(
|
|
pkg,
|
|
ver,
|
|
codename,
|
|
arch,
|
|
safe = !isTRUE(verdict$sensitive),
|
|
unsupported = verdict$unsupported
|
|
)
|
|
if (isTRUE(verdict$sensitive)) {
|
|
unsafe <- unsafe + 1L
|
|
} else {
|
|
safe <- safe + 1L
|
|
}
|
|
if (i %% 250L == 0L) {
|
|
cat(sprintf(
|
|
" inspected %d/%d (%d safe, %d unsafe, %d skipped)\n",
|
|
i,
|
|
nrow(compiled),
|
|
safe,
|
|
unsafe,
|
|
skipped
|
|
))
|
|
utils::flush.console()
|
|
}
|
|
}
|
|
|
|
cat(sprintf(
|
|
"Done: %d inspected, %d safe for every minor, %d unsafe, %d skipped\n",
|
|
nrow(compiled),
|
|
safe,
|
|
unsafe,
|
|
skipped
|
|
))
|