Skip to content

Cluster — daily recompute #89

Cluster — daily recompute

Cluster — daily recompute #89

Workflow file for this run

name: Cluster — daily recompute
# Runs the K-Means neighborhood-clustering job once a day.
# We can't rely on the API's in-process PeriodicTimer because the App Service
# is on the student tier (no Always On), so the runtime can sleep on idle.
# This workflow guarantees a recompute on a fixed schedule, independent of
# whether anyone is hitting the API.
on:
schedule:
# Every day at 03:00 UTC (06:00 Israel summer / 05:00 winter).
# Off-peak so the GIS calls run when TA's ArcGIS server is least loaded.
- cron: "0 3 * * *"
# Allow manual triggering from the Actions tab when you want to force a run.
workflow_dispatch:
permissions:
contents: read
jobs:
recompute:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Wake & cluster
env:
API_BASE: https://app-groundshare-api.azurewebsites.net
ADMIN_TOKEN: ${{ secrets.CLUSTERING_ADMIN_TOKEN }}
# How long to wait for the background recompute to finish, in seconds.
# The job is fire-and-forget server-side, so we poll diagnostics instead
# of holding one request open (which tripped Azure's 230s 504 timeout).
POLL_TIMEOUT: "480"
POLL_INTERVAL: "15"
run: |
set -euo pipefail
if [ -z "${ADMIN_TOKEN}" ]; then
echo "::error::CLUSTERING_ADMIN_TOKEN secret is not set."
exit 1
fi
# Extracts a top-level JSON field as a string ("" if null/missing).
json_field() { python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1') or '')"; }
# 1. Wake the API (student tier sleeps on idle). The diagnostics
# endpoint is anonymous and cheap, so it's a clean cold-start ping.
echo "Pinging diagnostics to warm the App Service..."
for attempt in 1 2 3; do
if curl -fsS --max-time 60 "${API_BASE}/api/clustering/diagnostics" >/tmp/diag.json; then
cat /tmp/diag.json; echo
break
fi
echo "Attempt ${attempt} failed, retrying..."
sleep 15
done
# Record the timestamp BEFORE we trigger, so we can detect when the
# background job has written a newer one.
before=$(json_field clustersLastUpdated </tmp/diag.json)
echo "clustersLastUpdated before run: '${before}'"
# 2. Kick off the recompute. The endpoint is fire-and-forget and returns
# 202 immediately (or while a run is already in progress).
echo "Triggering /run-now..."
http_code=$(curl -sS -o /tmp/run.json -w "%{http_code}" \
--max-time 60 \
-X POST \
-H "X-Admin-Token: ${ADMIN_TOKEN}" \
"${API_BASE}/api/clustering/run-now")
echo "HTTP ${http_code}"
cat /tmp/run.json; echo
# 202 Accepted is the success path; tolerate 200 for backward-compat
# during the deploy window when old/new code may briefly coexist.
if [ "${http_code}" != "202" ] && [ "${http_code}" != "200" ]; then
echo "::error::Cluster recompute trigger failed with HTTP ${http_code}"
exit 1
fi
# 3. Poll diagnostics until clustersLastUpdated advances past 'before'.
echo "Polling for completion (timeout ${POLL_TIMEOUT}s)..."
deadline=$(( $(date +%s) + POLL_TIMEOUT ))
while :; do
if curl -fsS --max-time 30 "${API_BASE}/api/clustering/diagnostics" >/tmp/diag.json; then
after=$(json_field clustersLastUpdated </tmp/diag.json)
if [ -n "${after}" ] && [ "${after}" != "${before}" ]; then
echo "Recompute complete. clustersLastUpdated now: '${after}'"
cat /tmp/diag.json; echo
exit 0
fi
echo " still running (clustersLastUpdated='${after}')..."
else
echo " diagnostics ping failed, will retry..."
fi
if [ "$(date +%s)" -ge "${deadline}" ]; then
echo "::error::Cluster recompute did not complete within ${POLL_TIMEOUT}s"
cat /tmp/diag.json 2>/dev/null || true
exit 1
fi
sleep "${POLL_INTERVAL}"
done