@@ -10,8 +10,15 @@ PROFILE ?= balanced
1010# sflux (GFS surface flux, native ~13 km, hourly, adds the dswrf layer), or
1111# hrrr (NOAA HRRR, 3 km over the contiguous US, a cycle every hour, to F18);
1212# mrms (the NOAA radar mosaic, an observation every two minutes) builds a
13- # window named by its first hour and has no live pointer yet .
13+ # window named by its first hour — `RUN=latest` the live rolling window .
1414MODEL ?= gfs
15+ # One round of a rolling window (`build-bin --round`): the run's artifacts
16+ # and manifest live in <model>.<run>/<ROUND>/ and the pointer names that
17+ # round, so a later round of the same run never overwrites what a viewer is
18+ # still reading (a range request against a rewritten object decodes the
19+ # wrong bytes). Empty for a forecast run, which is built once.
20+ ROUND ?=
21+ RUN_DIR = $(MODEL ) .$(RUN )$(if $(ROUND ) ,/$(ROUND ) )
1522# Published runs of one model to keep on R2 (`make prune-r2`). One means the
1623# live run only: the bucket carries no history.
1724KEEP ?= 1
@@ -37,7 +44,7 @@ AWS_REQUEST_CHECKSUM_CALCULATION ?= when_required
3744AWS_RESPONSE_CHECKSUM_VALIDATION ?= when_required
3845export AWS_DEFAULT_REGION AWS_REQUEST_CHECKSUM_CALCULATION AWS_RESPONSE_CHECKSUM_VALIDATION
3946
40- .PHONY : check install wasm test test-rust test-e2e encoder-rust encoder-rust-test encoder-wheel bench bench-video bench-lossy mvp serve format-pdf deploy-build upload-r2 upload-r2-bundles upload-r2-manifest check-pointer upload-r2-pointer warm-r2 prune-r2 live-run live-manifest deploy-pages deploy showcase showcase-check showcase-refresh upload-r2-showcase tc-build live-tc-index upload-r2-tc prune-r2-tc clean
47+ .PHONY : check install wasm test test-rust test-e2e encoder-rust encoder-rust-test encoder-wheel bench bench-video bench-lossy mvp serve format-pdf deploy-build upload-r2 upload-r2-bundles upload-r2-manifest check-pointer upload-r2-pointer warm-r2 prune-r2 prune-r2-rounds live-run live-manifest live-window deploy-pages deploy showcase showcase-check showcase-refresh upload-r2-showcase tc-build live-tc-index upload-r2-tc prune-r2-tc clean
4148
4249check :
4350 $(PYTHON ) scripts/check_dependencies.py
@@ -153,14 +160,18 @@ deploy-build:
153160# taking the run live with `upload-r2-manifest` — and the three targets share
154161# their pieces: a partial manifest (manifest.part.*.json, the assembler's
155162# input) never reaches the bucket from any of them.
163+ #
164+ # A rolling window's round (ROUND=HHMM) is the same path one directory
165+ # deeper: the round's artifacts and manifest are synced into
166+ # <model>.<run>/<ROUND>/ and the pointer names the round.
156167upload-r2 :
157- @set -e; dir=web/public/data/$(MODEL ) . $( RUN ); \
168+ @set -e; dir=web/public/data/$(RUN_DIR ) ; \
158169 [ -d " $$ dir" ] || { echo " no built run at $$ dir, pass RUN=YYYYMMDDHH" ; exit 1; }; \
159- $(MAKE ) --no-print-directory check-pointer MODEL=$(MODEL ) RUN=$(RUN ) ; \
160- $(S3 ) sync $$ dir s3://$(R2_BUCKET ) /$(R2_PREFIX ) /$(MODEL ) . $( RUN ) / --no-progress $(DRY_RUN ) \
170+ $(MAKE ) --no-print-directory check-pointer MODEL=$(MODEL ) RUN=$(RUN ) ROUND= $( ROUND ) ; \
171+ $(S3 ) sync $$ dir s3://$(R2_BUCKET ) /$(R2_PREFIX ) /$(RUN_DIR ) / --no-progress $(DRY_RUN ) \
161172 --exclude " manifest.part.*.json" \
162173 --cache-control " public, max-age=31536000, immutable" ; \
163- [ -n " $( DRY_RUN) " ] || $(MAKE ) --no-print-directory warm-r2 MODEL=$(MODEL ) RUN=$(RUN ) \
174+ [ -n " $( DRY_RUN) " ] || $(MAKE ) --no-print-directory warm-r2 MODEL=$(MODEL ) RUN=$(RUN ) ROUND= $( ROUND ) \
164175 || echo " warming the edge cache failed; the run goes live cold" ; \
165176 $(MAKE ) --no-print-directory upload-r2-pointer MODEL=$(MODEL ) RUN=$(RUN ) DRY_RUN=$(DRY_RUN )
166177
@@ -199,12 +210,16 @@ upload-r2-manifest:
199210# both, and a pointer that disagrees with its manifest strands every viewer
200211# on a 404.
201212check-pointer :
202- @set -e; dir=web/public/data/$(MODEL ) . $( RUN ); \
213+ @set -e; dir=web/public/data/$(RUN_DIR ) ; \
203214 pointer_run=$$(jq -r .run web/public/data/$(LATEST_FILE ) ) ; \
204215 [ " $$ pointer_run" = " $( RUN) " ] || { \
205216 echo " $( LATEST_FILE) names run $$ pointer_run, not $( RUN) — a later build rewrote it;" ; \
206217 echo " rebuild run $( RUN) (or upload run $$ pointer_run) so the pointer matches the assets" ; \
207218 exit 1; }; \
219+ pointer_path=$$(jq -r .manifestPath web/public/data/$(LATEST_FILE ) ) ; \
220+ [ " $$ pointer_path" = " $( RUN_DIR) /manifest.json" ] || { \
221+ echo " $( LATEST_FILE) names $$ pointer_path, not $( RUN_DIR) /manifest.json — pass the ROUND it was built with" ; \
222+ exit 1; }; \
208223 pointer_crc=$$(jq -r .manifestCrc32 web/public/data/$(LATEST_FILE ) ) ; \
209224 manifest_crc=$$($(PYTHON ) -c "import sys, zlib; print(f'{zlib.crc32(open(sys.argv[1], \"rb\") .read()) & 0xFFFFFFFF:08x}' )" $$dir/manifest.json); \
210225 [ " $$ pointer_crc" = " $$ manifest_crc" ] || { \
@@ -221,7 +236,7 @@ upload-r2-pointer:
221236# the site's Origin header, so the edge (and, with tiered cache, the upper
222237# tier every other data center fills from) holds it before anyone asks.
223238warm-r2 :
224- scripts/warm_edge_cache.sh $(MODEL ) $(RUN )
239+ ROUND= $( ROUND ) scripts/warm_edge_cache.sh $(MODEL ) $(RUN )
225240
226241# Historical showcase cases: past runs cropped to one weather event, defined
227242# in showcase/cases/*.json and built into web/public/data/showcase/. Pass
@@ -339,6 +354,31 @@ prune-r2:
339354 fi ; \
340355 done
341356
357+ # A rolling window keeps its rounds beside one another inside the run
358+ # directory. After the pointer has moved on to a new round, the rounds
359+ # before the newest ROUNDS_KEEP are deleted — never the one the pointer
360+ # names, whatever its name sorts as. A viewer on a replaced round has the
361+ # next round plus a poll interval to be brought forward before its objects
362+ # go; the top-level prune (`prune-r2`, KEEP=2 for this source) is what
363+ # keeps the previous run's last round alongside.
364+ ROUNDS_KEEP ?= 2
365+ prune-r2-rounds :
366+ @set -e; \
367+ pointer=$$($(S3 ) cp s3://$(R2_BUCKET ) /$(R2_PREFIX ) /$(LATEST_FILE ) - --only-show-errors 2>/dev/null || true) ; \
368+ [ -n " $$ pointer" ] || { echo " no live pointer for $( MODEL) , refusing to prune" ; exit 1; }; \
369+ live=$$(printf '%s' "$$pointer" | jq -r .manifestPath | xargs dirname ) ; \
370+ run=$$(printf '%s' "$$pointer" | jq -r .run ) ; \
371+ echo " live $( MODEL) round: $$ live" ; \
372+ listing=$$($(S3 ) ls s3://$(R2_BUCKET ) /$(R2_PREFIX ) /$(MODEL ) .$$run/) \
373+ || { echo " listing the run failed, refusing to prune" ; exit 1; }; \
374+ for round in $$ (printf ' %s\n' " $$ listing" | awk ' / PRE /{print $$2}' \
375+ | sed ' s:/$$::' | sort -r | tail -n +$$(($(ROUNDS_KEEP ) + 1 ) )); do \
376+ if [ " $( MODEL) .$$ run/$$ round" != " $$ live" ]; then \
377+ echo " Deleting $( MODEL) .$$ run/$$ round..." ; \
378+ $(S3 ) rm s3://$(R2_BUCKET ) /$(R2_PREFIX ) /$(MODEL ) .$$ run/$$ round/ --recursive --only-show-errors $(DRY_RUN ) ; \
379+ fi ; \
380+ done
381+
342382# Print the run the live pointer names, or nothing when there is no pointer.
343383live-run :
344384 @$(S3 ) cp s3://$(R2_BUCKET ) /$(R2_PREFIX ) /$(LATEST_FILE ) - --only-show-errors | jq -r .run || true
@@ -347,9 +387,17 @@ live-run:
347387# base a top-up merges new bundles onto (`xuebuild assemble-run
348388# --base-manifest`). Prints nothing when there is no live run.
349389live-manifest :
350- @set -e; live=$$($(MAKE ) -s --no-print-directory live-run MODEL=$(MODEL ) ) ; \
351- [ -n " $$ live" ] || exit 0; \
352- $(S3 ) cp s3://$(R2_BUCKET ) /$(R2_PREFIX ) /$(MODEL ) .$$ live/manifest.json - --only-show-errors
390+ @set -e; path=$$($(S3 ) cp s3://$(R2_BUCKET ) /$(R2_PREFIX ) /$(LATEST_FILE ) - --only-show-errors 2>/dev/null | jq -r .manifestPath || true) ; \
391+ [ -n " $$ path" ] || exit 0; \
392+ $(S3 ) cp s3://$(R2_BUCKET ) /$(R2_PREFIX ) /$$ path - --only-show-errors
393+
394+ # The window.json beside the live manifest of a rolling window (what the
395+ # round holds: frame count, first and newest slot), as the bucket holds it.
396+ # Prints nothing when there is no live run or the run has no window record.
397+ live-window :
398+ @set -e; path=$$($(S3 ) cp s3://$(R2_BUCKET ) /$(R2_PREFIX ) /$(LATEST_FILE ) - --only-show-errors 2>/dev/null | jq -r .manifestPath || true) ; \
399+ [ -n " $$ path" ] || exit 0; \
400+ $(S3 ) cp s3://$(R2_BUCKET ) /$(R2_PREFIX ) /$$(dirname $$path ) /window.json - --only-show-errors 2> /dev/null || true
353401
354402# Publish dist-deploy/ (built via deploy-build) to the Cloudflare Pages project.
355403deploy-pages :
0 commit comments