Skip to content

Commit ec5ae94

Browse files
authored
Merge pull request #77 from nextstrain/single-pass-upload
upload-to-s3: read the source once instead of three times
2 parents d711b08 + f1d9713 commit ec5ae94

1 file changed

Lines changed: 54 additions & 14 deletions

File tree

scripts/upload-to-s3

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ set -euo pipefail
33

44
bin="$(dirname "$0")"
55

6+
# Temp artifacts, cleaned up however the script exits. Globals so the EXIT trap
7+
# sees them regardless of where in main() we leave (return, `exit`, or set -e).
8+
tmp=""
9+
workdir=""
10+
trap '[[ -n "$workdir" ]] && rm -rf "$workdir"; [[ -n "$tmp" ]] && rm -f "$tmp"' EXIT
11+
612
main() {
713
local quiet=0
814

@@ -20,28 +26,62 @@ main() {
2026
local dst="${2:?A destination s3:// URL is required as the second argument.}"
2127
local cloudfront_domain="${3:-}"
2228

29+
# Fail early if $src is unreadable. Otherwise `tee < "$src"` below fails, but
30+
# the background hasher/counter -- already blocked opening their fifos -- get
31+
# no writer and hang instead of being cleaned up.
32+
[[ -r "$src" ]] || { echo "upload-to-s3: cannot read source file: $src" >&2; exit 1; }
33+
2334
local s3path="${dst#s3://}"
2435
local bucket="${s3path%%/*}"
2536
local key="${s3path#*/}"
2637

27-
local src_hash dst_hash no_hash=0000000000000000000000000000000000000000000000000000000000000000
28-
src_hash="$("$bin/sha256sum" < "$src")"
29-
dst_hash="$(aws s3api head-object --bucket "$bucket" --key "$key" --query Metadata.sha256sum --output text 2>/dev/null || echo "$no_hash")"
38+
# Read $src exactly once: while compressing it to a temp file, tee the same
39+
# bytes to the hasher and the line counter. The old code read $src three
40+
# separate times (sha256sum, wc -l, zstd); for the large aligned/sequences
41+
# files those reads -- not the S3 transfer -- dominated the upload rules.
42+
#
43+
# Uploading a real file (rather than piping to `aws s3 cp -`) also lets the
44+
# CLI parallelize the multipart transfer; a stdin stream can't seek and
45+
# degrades to one slow stream. The temp lives next to $src (same big volume).
46+
local upload_file="$src"
47+
workdir="$(mktemp -d)"
3048

31-
if [[ $src_hash != "$dst_hash" ]]; then
32-
# The record count may have changed
49+
local -a compressor=()
50+
case "$dst" in
51+
*.gz) compressor=(gzip -c) ;;
52+
*.xz) compressor=(xz -2 -T0 -c) ;;
53+
*.zst) compressor=(zstd -T0 -c) ;;
54+
esac
55+
56+
local src_hash src_record_count
57+
if [[ ${#compressor[@]} -gt 0 ]]; then
58+
tmp="$(mktemp "$src.upload.XXXXXX")"
59+
upload_file="$tmp"
60+
mkfifo "$workdir/hash.fifo" "$workdir/count.fifo"
61+
"$bin/sha256sum" < "$workdir/hash.fifo" > "$workdir/hash" &
62+
local hash_pid=$!
63+
wc -l < "$workdir/count.fifo" > "$workdir/count" &
64+
local count_pid=$!
65+
tee "$workdir/hash.fifo" "$workdir/count.fifo" < "$src" | "${compressor[@]}" > "$tmp"
66+
# Wait one at a time: `wait a b` returns only b's status, so a failing
67+
# hasher would be masked from set -e (uploading with a bad sha256sum).
68+
wait "$hash_pid"
69+
wait "$count_pid"
70+
src_hash="$(cat "$workdir/hash")"
71+
src_record_count="$(cat "$workdir/count")"
72+
else
73+
# No compression (e.g. the small *.json files) -- hash and count directly.
74+
src_hash="$("$bin/sha256sum" < "$src")"
3375
src_record_count="$(wc -l < "$src")"
76+
fi
77+
78+
local dst_hash no_hash=0000000000000000000000000000000000000000000000000000000000000000
79+
dst_hash="$(aws s3api head-object --bucket "$bucket" --key "$key" --query Metadata.sha256sum --output text 2>/dev/null || echo "$no_hash")"
3480

81+
if [[ $src_hash != "$dst_hash" ]]; then
3582
echo "Uploading $src$dst"
36-
if [[ "$dst" == *.gz ]]; then
37-
gzip -c "$src"
38-
elif [[ "$dst" == *.xz ]]; then
39-
xz -2 -T0 -c "$src"
40-
elif [[ "$dst" == *.zst ]]; then
41-
zstd -T0 -c "$src"
42-
else
43-
cat "$src"
44-
fi | aws s3 cp --no-progress - "$dst" --metadata sha256sum="$src_hash",recordcount="$src_record_count" "$(content-type "$dst")"
83+
84+
aws s3 cp --no-progress "$upload_file" "$dst" --metadata sha256sum="$src_hash",recordcount="$src_record_count" "$(content-type "$dst")"
4585

4686
if [[ -n $cloudfront_domain ]]; then
4787
echo "Creating CloudFront invalidation for $cloudfront_domain/$key"

0 commit comments

Comments
 (0)