Skip to content

Instantly share code, notes, and snippets.

@Integralist
Last active July 16, 2024 06:16
Show Gist options
  • Save Integralist/87118a8f79d47aaf640c21149bf9d687 to your computer and use it in GitHub Desktop.
Save Integralist/87118a8f79d47aaf640c21149bf9d687 to your computer and use it in GitHub Desktop.
[Fastly create, validate, and destroy service] #CLI #Fastly

Compute readthrough cache validator

This directory contains a Compute application that proxies incoming requests onto https://http-me.glitch.me/.

There is a run.sh script which will attempt to validate the responses from the Compute service to see what cache semantics are respected.

Tip

Read the official Fastly documentation: readthrough cache

The run.sh script does the following:

  • Checks if real is passed as an input argument.
    • If yes, it attempts to deploy the Compute application to Fastly.
    • Otherwise, it attempts to run the Compute application locally.
  • It makes multiple requests to the Compute application.
  • It validates that the responses are as expected.

Important

It doesn't make sense (currently) to try and run this script without real as the input argument, because it causes the script to run the Compute application locally using fastly compute serve, which itself uses https://github.com/fastly/viceroy/ and Viceroy (at the moment) has no support for cache semantics.

POPs and Retries

A request that you might expect to return a cache HIT, could return a MISS. This is because the request can end up at a different POP to where a previous request for the resource ended up.

For example, in the UK there are multiple POPs. Nearest to me are the LHR and LCY POPs. This means I can make a request that ends up at the LHR POP, and if I make a second request and it also ends up at the same POP, then I'll get a cache HIT, otherwise if the request ends up at the LCY POP I'll get a cache MISS.

To try and account for this the run.sh script will re-attempt the request a number of times before marking it as unsuccessful. Ultimately, we want to be sure a request is either cached or not cached, so depending on what the expectation is, we give the script the best chance possible to validate the expectation accurately.

Summary of results

Refer to the run.sh script for the details.

Request Method Response Code Response Headers Cacheable
GET 200
GET 200 Cache-Control:max-age=120
GET 200 Surrogate-Control:max-age=120
GET 200 Cache-Control:max-age=120&Surrogate-Control:max-age=120
GET 200 Set-Cookie:foo=bar
GET 200 Cache-Control:no-store
GET 200 Cache-Control:private
GET 200 Surrogate-Control:no-store
GET 200 Surrogate-Control:private
GET 203
GET 300
GET 301
GET 302
GET 400
GET 404
GET 410
GET 500
GET 503
POST 200
POST 200 Cache-Control:max-age=120
POST 200 Surrogate-Control:max-age=120
POST 200 Cache-Control:max-age=120&Surrogate-Control:max-age=120

Note

The Fastly VCL documentation suggests a 302 is cacheable, but it's not in Compute.

#!/usr/bin/env bash
real="$1"
cleanup() {
if [ "$real" == "real" ]; then
echo ""
fastly service delete --force # uses service_id in fastly.toml
fi
}
trap 'cleanup' ERR
if [ "$real" == "real" ]; then
fastly compute publish --non-interactive # uses [setup] in fastly.toml to create backend resource
else
fastly compute serve --verbose & # run in the background
bg_pid=$! # store the Fastly CLI's Process ID
fi
if [ "$real" == "real" ]; then
service_id=$(yq eval '.service_id' fastly.toml)
domain=$(fastly domain list --service-id "$service_id" --version latest --json | jq -r '.[0].Name')
endpoint="https://$domain"
else
endpoint="http://127.0.0.1:7676"
fi
if [ "$real" != "real" ]; then
# wait for the `serve` command to have spun up a local server
server_port=7676
max_attempts=10
attempt=0
while ! nc -z localhost "$server_port"; do
if (( attempt == max_attempts )); then
echo ""
echo "The local server did not start within the specified number of attempts."
kill "$bg_pid" # terminate the Fastly CLI running `serve` command in the background
sleep 2 # give just enough time for Viceroy to setup its listener
kill "$(lsof -i :7676 | awk 'NR==2 {print $2}')" # terminal Viceroy (CLI might not have a chance to setup signal monitoring to terminate it yet)
exit 1
fi
sleep 1
(( attempt++ ))
done
fi
# NOTE: "Fastly-Debug:1" forces the display of the `Surrogate-Control` header.
# We don't set Fastly-Debug because we want to validate Surrogate-Control is omitted from the response.
#
# IMPORTANT: Compute doesn't strip Surrogate-Control for POST requests.
# This is to support VCL service chaining where VCL needs to cache the response.
# Meaning the VCL service requires the Surrogate-Control still.
# The Compute team will investigate if it's possible to fix this so that a
# Compute service will strip the header if not fronted by another Fastly
# service. Now, although POST requests don't strip Surrogate-Control and GET
# requests do, the Viceroy testing tool NEVER strips Surrogate-Control and this
# appears to be related to the fact that it has no cache semantics support.
function check_cacheable() {
local url=$1
local needle="x-cache: HIT"
retries=5
while [ "$retries" -gt 0 ]; do
response=$(curl -D - -s "$url")
if [[ $url == *"Surrogate-Control"* ]]; then
if [[ $response == *"surrogate-control"* ]]; then
echo ""
echo "❌ Surrogate-Control failed to be stripped from the response for $url"
echo ""
fi
fi
if [[ $url == *"Cache-Control"* ]]; then
if [[ $response != *"cache-control"* ]]; then
echo ""
echo "❌ Cache-Control failed to be found in the response for $url"
echo ""
fi
fi
if [[ $response == *"$needle"* ]]; then
echo ""
echo "✅ Found '$needle' in the response from $url"
echo ""
success="true"
break
else
((retries--))
success="false"
sleep 1
fi
done
if [ "$success" != "true" ]; then
echo ""
echo "❌ Failed after 5 retries to find '$needle' in the response from $url"
echo ""
fi
}
echo ""
echo "Validating cacheable endpoints..."
check_cacheable "$endpoint/anything/status=200"
check_cacheable "$endpoint/anything/status=203"
check_cacheable "$endpoint/anything/status=300"
check_cacheable "$endpoint/anything/status=301"
check_cacheable "$endpoint/anything/status=404"
check_cacheable "$endpoint/anything/status=410"
check_cacheable "$endpoint/anything/status=200?header=Cache-Control:max-age=120"
check_cacheable "$endpoint/anything/status=200?header=Surrogate-Control:max-age=120"
check_cacheable "$endpoint/anything/status=200?header=Surrogate-Control:max-age=240&header=Cache-Control:max-age=120"
function check_uncacheable() {
local url=$1
local method=${2:-"GET"}
local needle="x-cache: HIT"
retries=5
surrogate_error_displayed="false"
cache_error_displayed="false"
while [ "$retries" -gt 0 ]; do
response=$(curl -X "$method" -D - -s "$url")
if [[ $url == *"Surrogate-Control"* && $surrogate_error_displayed == "false" && $method != "POST" ]]; then
if [[ $response == *"surrogate-control"* ]]; then
echo ""
echo "❌ Surrogate-Control failed to be stripped from the response for $method $url"
echo ""
surrogate_error_displayed="true"
fi
fi
if [[ $url == *"Cache-Control"* && $cache_error_displayed == "false" ]]; then
if [[ $response != *"cache-control"* ]]; then
echo ""
echo "❌ Cache-Control failed to be found in the response for $method $url"
echo ""
cache_error_displayed="true"
fi
fi
if [[ $response == *"$needle"* ]]; then
echo ""
echo "❌ Found '$needle' in the response from $method $url"
echo ""
success="false"
break
else
((retries--))
success="true"
sleep 1
fi
done
if [ "$success" == "true" ]; then
echo ""
echo "✅ After 5 retries '$needle' was NOT found in the response from $method $url"
echo ""
fi
}
echo "Validating uncacheable endpoints..."
check_uncacheable "$endpoint/anything/status=200?header=Set-Cookie:foo=bar"
check_uncacheable "$endpoint/anything/status=200?header=Cache-Control:no-store"
check_uncacheable "$endpoint/anything/status=200?header=Cache-Control:private"
check_uncacheable "$endpoint/anything/status=200?header=Surrogate-Control:no-store"
check_uncacheable "$endpoint/anything/status=200?header=Surrogate-Control:private"
check_uncacheable "$endpoint/anything/status=200" "POST"
check_uncacheable "$endpoint/anything/status=200?header=Cache-Control:max-age=120" "POST"
check_uncacheable "$endpoint/anything/status=200?header=Surrogate-Control:max-age=120" "POST"
check_uncacheable "$endpoint/anything/status=200?header=Surrogate-Control:max-age=240&header=Cache-Control:max-age=120" "POST"
check_uncacheable "$endpoint/anything/status=302" # https://www.fastly.com/documentation/reference/vcl/variables/backend-response/beresp-cacheable/ suggested this was cacheable, but it's not
check_uncacheable "$endpoint/anything/status=400"
check_uncacheable "$endpoint/anything/status=500"
check_uncacheable "$endpoint/anything/status=503"
if [ "$real" != "real" ]; then
kill "$bg_pid" # terminate the Fastly CLI running `serve` command in the background
kill "$(lsof -i :7676 | awk 'NR==2 {print $2}')" 2>/dev/null # terminal Viceroy if still running (although CLI should have signals setup at this point and would have terminated it already)
fi
cleanup
# NOTE: 3600s (1hr) is XQD's default TTL (VCL services have a 2min TTL).
package main
import (
"context"
"fmt"
"time"
"github.com/fastly/compute-sdk-go/fsthttp"
)
// BackendName is the origin server incoming requests will be proxied onto.
const BackendName = "httpme"
func main() {
fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) {
start := time.Now()
resp, err := r.Send(ctx, BackendName)
if err != nil {
w.WriteHeader(fsthttp.StatusBadGateway)
fmt.Fprintln(w, err.Error())
return
}
w.Header().Reset(resp.Header)
w.Header().Set("X-Execution-Time", time.Since(start).String())
w.WriteHeader(resp.StatusCode)
if err := w.Append(resp.Body); err != nil {
w.WriteHeader(fsthttp.StatusBadGateway)
fmt.Fprintln(w, err.Error())
return
}
})
}
# This file describes a Fastly Compute package. To learn more visit:
# https://developer.fastly.com/reference/fastly-toml/
authors = ["integralist@fastly.com"]
cloned_from = "https://github.com/fastly/compute-starter-kit-go-default"
description = ""
language = "go"
manifest_version = 3
name = "fastly-readthrough-cache"
service_id = ""
[local_server]
[local_server.backends]
[local_server.backends.httpme]
override_host = "http-me.glitch.me"
url = "https://http-me.glitch.me/"
[scripts]
build = "go build -o bin/main.wasm ."
env_vars = ["GOARCH=wasm", "GOOS=wasip1"]
[setup]
[setup.backends]
[setup.backends.httpme]
address = "http-me.glitch.me"
description = "HTTP me is a tiny express app initally designed to replicate the features of HTTPBin.org"
port = 443
module github.com/domainr/fastly-readthrough-cache
go 1.22
require github.com/fastly/compute-sdk-go v1.3.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment