Skip to content

fix(recovery): guard claim fan-out send on context cancellation to unblock Stop #81

fix(recovery): guard claim fan-out send on context cancellation to unblock Stop

fix(recovery): guard claim fan-out send on context cancellation to unblock Stop #81

name: Token Validation Benchmark
# Runs the token-validation benchmarks on both the PR branch and its base
# branch, then posts a comment on the PR with the percentage differences.
#
# Run locally with act (https://github.com/nektos/act):
# 1. Create an event payload describing the PR to benchmark. base.sha/head.sha
# must be commits that exist locally; number is only used for the comment.
# cat > /tmp/pr-event.json <<'JSON'
# {
# "pull_request": {
# "number": 1,
# "base": { "sha": "<base-commit-sha>" },
# "head": { "sha": "<pr-commit-sha>" }
# }
# }
# JSON
# 2. Run one variant at a time so the two gRPC jobs never share port 8099
# (act puts all matrix jobs on the host network; real CI isolates them):
# act pull_request \
# -W .github/workflows/token-validation-benchmark.yml \
# -e /tmp/pr-event.json \
# --matrix variant:ipa \
# --artifact-server-path /tmp/act-artifacts
# 3. The final `gh pr comment` step needs a real token/PR; expect it to fail
# locally. Inspect the generated report in the compare job's logs instead,
# or add `--matrix ...` and run only the `benchmark` job with `-j benchmark`.
# If a run is interrupted, clear leftover containers before retrying:
# docker rm -f $(docker ps -aq --filter ancestor=catthehacker/ubuntu:act-latest)
on:
# pull_request_target runs in the BASE repo context, so GITHUB_TOKEN keeps
# write access even for fork PRs — which is what lets the compare job post its
# comment. The tradeoff is that this event is inherently more dangerous: the
# benchmark job below checks out and runs the PR HEAD (untrusted) code. To
# keep that safe, the default token here is read-only (see permissions), and
# pull-requests: write is granted ONLY to the trusted compare job that never
# runs PR code. Do not add secrets or write scopes to the benchmark job.
pull_request_target:
workflow_dispatch:
# Read-only by default. The benchmark job runs untrusted PR head code, so it
# must not have a write-capable token. Write access is scoped per-job on the
# compare job (below), which only runs trusted base-repo code.
permissions:
contents: read
env:
# -mod=mod lets `go test` update go.mod/go.sum in the ephemeral CI checkout.
GOFLAGS: -mod=mod
MODULE_DIR: cmd/token_validation_service
GO_MOD_FILE: cmd/token_validation_service/go.mod
OUTPUT_DIR: bench
# go test knobs. Kept modest so the PR feedback loop stays reasonable; raise
# COUNT for more statistically stable comparisons.
BENCHTIME: 10s
COUNT: 5
# -cpu is set at run time to the number of CPUs the runner actually has
# (nproc), rather than a fixed 16/32 that may exceed the runner and skew
# results. NUM_CONN is the gRPC client connection sweep, unrelated to -cpu.
NUM_CONN: 4
# -testRoot values selecting the crypto variant. Each variant now names its
# testdata explicitly rather than relying on a compiled-in default.
CSP_TEST_ROOT: -testRoot=../../token/core/zkatdlog/nogh/v1/regression/testdata/zero/csp/32-BLS12_381_BBS_GURVY
IPA_TEST_ROOT: -testRoot=../../token/core/zkatdlog/nogh/v1/regression/testdata/zero/32-BLS12_381_BBS_GURVY
# GOGC for the benchmark runs. A high value suppresses GC during the short
# benchtime windows so measurements aren't perturbed by collection pauses.
GOGC: 10000
# Percentage change treated as noise by the compare step. Deltas smaller than
# this (or whose base/PR sample ranges overlap) are reported as neutral (➖).
DELTA: 1
jobs:
# Job 1: benchmark BOTH branches on the SAME runner, interleaved.
#
# The matrix is the cross product of:
# * variant - the crypto backend (ipa is the default, csp adds -testRoot)
# * bench - which benchmark to run and the flags unique to it
#
# giving (2 variants x 2 benchmarks) = 4 runs. Crucially, `ref` (pr vs base)
# is NOT a matrix dimension: each job checks out both the PR head and the base
# commit and benchmarks them on the one runner, sample-by-sample interleaved.
# This is deliberate — GitHub-hosted runners sit on heterogeneous host CPUs
# with noisy neighbors, so benchmarking pr and base on two different VMs made
# the reported delta a measure of hardware variance (±10-25%) rather than of
# the diff. Same-runner + interleaving cancels both host-to-host variance and
# within-runner drift, so the ratio reflects the code change.
#
# Because the two runs on a runner are sequential (not parallel), the fixed
# gRPC server port never collides.
benchmark:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
variant: [ipa, csp]
bench:
# Per-benchmark flags are assembled in the run step (below) because
# the ${{ env }} context is not available inside a matrix.
- id: local
name: BenchmarkLocalTokenValidation
file_prefix: local-token-validation
- id: grpc
name: BenchmarkAPIGRPC
file_prefix: api-grpc
env:
# The _pr_ / _base_ tag in the filename is how the compare job groups
# results back into the two branches.
BASE_FILE: ${{ matrix.bench.file_prefix }}-${{ matrix.variant }}_base_.txt
PR_FILE: ${{ matrix.bench.file_prefix }}-${{ matrix.variant }}_pr_.txt
steps:
- name: Checkout base (${{ github.event.pull_request.base.sha }})
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.sha }}
path: base
- name: Checkout pr (${{ github.event.pull_request.head.sha }})
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
path: pr
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: pr/${{ env.GO_MOD_FILE }}
cache-dependency-path: "**/*.sum"
- name: Run ${{ matrix.bench.name }} (${{ matrix.variant }}, base vs pr interleaved)
run: |
set -o pipefail
mkdir -p "$GITHUB_WORKSPACE/$OUTPUT_DIR"
# Use exactly the CPUs the runner has, so -cpu never exceeds the
# available parallelism (which would oversubscribe and add noise).
cpu=$(nproc)
# Flags unique to each benchmark:
# local - in-process validation, no server, -cpu = runner CPUs.
# grpc - full gRPC API path, -cpu = runner CPUs, sweep client conns.
case "${{ matrix.bench.id }}" in
local) bench_flags="-run ^$ -cpu=$cpu" ;;
grpc) bench_flags="-cpu=$cpu -numConn=$NUM_CONN" ;;
esac
# Each variant selects its crypto testdata explicitly.
case "${{ matrix.variant }}" in
ipa) test_root="$IPA_TEST_ROOT" ;;
csp) test_root="$CSP_TEST_ROOT" ;;
esac
base_out="$GITHUB_WORKSPACE/$OUTPUT_DIR/$BASE_FILE"
pr_out="$GITHUB_WORKSPACE/$OUTPUT_DIR/$PR_FILE"
: > "$base_out"
: > "$pr_out"
# Run one sample of base, then one of pr, and repeat COUNT times.
# Interleaving (base,pr,base,pr,...) cancels slow drift on the runner
# in addition to the host-to-host variance the same-runner design
# already removes. Each file ends up with COUNT benchmark lines, which
# is shape-identical to `go test -count=COUNT` for the parser.
run_one() {
# $1 = checkout dir, $2 = output file to append to
( cd "$1/$MODULE_DIR" && go test \
-bench=${{ matrix.bench.name }} \
-benchtime="$BENCHTIME" \
-count=1 \
$bench_flags \
$test_root \
) | tee -a "$2"
}
for i in $(seq 1 "$COUNT"); do
echo "=== sample $i/$COUNT: base ==="
run_one base "$base_out"
echo "=== sample $i/$COUNT: pr ==="
run_one pr "$pr_out"
done
- name: Upload raw benchmark output
uses: actions/upload-artifact@v4
with:
name: benchmark-raw-${{ matrix.bench.id }}-${{ matrix.variant }}
path: |
${{ env.OUTPUT_DIR }}/${{ env.BASE_FILE }}
${{ env.OUTPUT_DIR }}/${{ env.PR_FILE }}
if-no-files-found: error
# Job 2: pair PR-vs-base results and post the percentage differences.
compare:
needs: benchmark
runs-on: ubuntu-latest
# Write scope lives HERE, not at the top level, because this job runs only
# trusted base-repo code (the checkout below takes the default base ref, and
# it never checks out or executes PR head code). The benchmark job keeps the
# read-only default token.
permissions:
contents: read
pull-requests: write
continue-on-error: true
steps:
# No ref specified: under pull_request_target this checks out the BASE
# repo/branch (trusted), which is exactly what we want for the code that
# holds the write token.
- name: Checkout code
uses: actions/checkout@v4
- name: Download raw benchmark outputs
uses: actions/download-artifact@v4
with:
pattern: benchmark-raw-*
path: ${{ env.OUTPUT_DIR }}
merge-multiple: true
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install parser dependencies
run: pip install pandas
- name: Build comparison report
run: |
python cmd/benchmarking/compare_benchmarks.py \
--input-dir "$OUTPUT_DIR" \
--base-tag '_base_' \
--pr-tag '_pr_' \
--delta "$DELTA" \
--output comment.md
- name: Post comparison comment
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh pr comment "${{ github.event.pull_request.number }}" --body-file comment.md