From 1874ebe93b03e7bce2396cf16a3816b337907d1e Mon Sep 17 00:00:00 2001 From: Stefano Scafiti Date: Tue, 23 Jun 2026 13:07:55 +0200 Subject: [PATCH 1/2] Add --rerender flag to run command When set, each script is re-rendered from its source file at the start of every cycle through its steps, regenerating template values such as randomObjectID for each cycle instead of baking them in once at startup. - scriptSource records a script's origin (file, vars, document index) and re-renders it on demand via config.Load + Prepare. - StepForward swaps in a freshly rendered script at each cycle boundary (numExecuted % len(steps) == 0); render errors log a warning and reuse the previous render. - RunWorker copies the source into each per-worker context, so re-rendering is per-worker. --- internal/cmd/run.go | 6 +++ internal/thumperrunner/exec.go | 63 +++++++++++++++++++++++++++++++ internal/thumperrunner/thumper.go | 1 + 3 files changed, 70 insertions(+) diff --git a/internal/cmd/run.go b/internal/cmd/run.go index 1a61f47..b742b53 100644 --- a/internal/cmd/run.go +++ b/internal/cmd/run.go @@ -36,6 +36,7 @@ func RegisterRunFlags(cmd *cobra.Command) { cmd.Flags().Int("qps", 1, "queries per second to generate") cmd.Flags().Duration("step-timeout", 500*time.Millisecond, "maximum time a single step is allowed to run") cmd.Flags().Bool("randomize-starting-step", false, "randomize the starting script step for each worker") + cmd.Flags().Bool("rerender", false, "re-render each script from its source file at the start of every cycle through its steps, regenerating template values such as randomObjectID for each cycle instead of once at startup") // Register http flags MetricsServerBuilder.RegisterFlags(cmd.Flags()) @@ -63,6 +64,7 @@ func runCmdFunc(cmd *cobra.Command, args []string) error { qps := cobrautil.MustGetInt(cmd, "qps") stepTimeout := cobrautil.MustGetDuration(cmd, "step-timeout") stepRandomization := cobrautil.MustGetBool(cmd, "randomize-starting-step") + rerender := cobrautil.MustGetBool(cmd, "rerender") psName := cobrautil.MustGetString(cmd, "permissions-system") log.Info().Int("qps", qps).Str("permission-system", psName).Msg("starting run command") @@ -102,6 +104,10 @@ func runCmdFunc(cmd *cobra.Command, args []string) error { return fmt.Errorf("error preparing scripts for execution: %w", err) } + if rerender { + thumperrunner.EnableRerender(preparedFileScripts, scriptFilename, scriptVars) + } + if !usedRandom { scriptCache[scriptFilename] = preparedFileScripts } diff --git a/internal/thumperrunner/exec.go b/internal/thumperrunner/exec.go index 5745526..75619d5 100644 --- a/internal/thumperrunner/exec.go +++ b/internal/thumperrunner/exec.go @@ -5,6 +5,8 @@ import ( "fmt" "time" + "github.com/authzed/internal/thumper/internal/config" + v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" "github.com/authzed/authzed-go/v1" "github.com/rs/zerolog/log" @@ -22,6 +24,10 @@ type ExecutableScript struct { name string weight uint steps []executableStep + + // source, when set, allows the script to be re-rendered from its origin + // file at the start of each execution cycle (see EnableRerender). + source *scriptSource } type ExecutableContext struct { @@ -30,10 +36,67 @@ type ExecutableContext struct { numExecuted int zedToken *v1.ZedToken + + // source is copied from the script when the context is created; when + // non-nil the script is re-rendered at the start of each cycle. + source *scriptSource +} + +// scriptSource records where a script was loaded from so it can be re-rendered +// (re-templated) on demand, regenerating template values such as randomObjectID. +type scriptSource struct { + filename string + vars config.ScriptVariables + docIndex int +} + +// load re-renders the source file and returns a freshly prepared copy of the +// document at docIndex. +func (src *scriptSource) load() (*ExecutableScript, error) { + scripts, _, err := config.Load(src.filename, src.vars) + if err != nil { + return nil, fmt.Errorf("error re-loading script %s: %w", src.filename, err) + } + + prepared, err := Prepare(scripts) + if err != nil { + return nil, fmt.Errorf("error re-preparing script %s: %w", src.filename, err) + } + + if src.docIndex >= len(prepared) { + return nil, fmt.Errorf("script %s no longer contains document %d", src.filename, src.docIndex) + } + + return prepared[src.docIndex], nil +} + +// EnableRerender attaches a re-render source to each prepared script so that, +// at the start of every cycle through its steps, the script is re-rendered from +// filename. This regenerates template values (notably randomObjectID) for each +// cycle instead of baking them in once at load time. +func EnableRerender(scripts []*ExecutableScript, filename string, vars config.ScriptVariables) { + for idx, s := range scripts { + s.source = &scriptSource{filename: filename, vars: vars, docIndex: idx} + } } // StepForward advances the script one step and then stops. func (s *ExecutableContext) StepForward(workerIndex int, stepTimeout time.Duration) { + // At the start of each cycle through the script's steps, re-render the + // script from its source (if enabled) so template values such as + // randomObjectID are regenerated for this cycle. + if s.source != nil && s.numExecuted%len(s.script.steps) == 0 { + if fresh, err := s.source.load(); err != nil { + log.Warn(). + Err(err). + Str("script", s.script.name). + Int("worker", workerIndex). + Msg("failed to re-render script; reusing previous render") + } else { + s.script = fresh + } + } + ctx, cancel := context.WithTimeout(context.Background(), stepTimeout) defer cancel() diff --git a/internal/thumperrunner/thumper.go b/internal/thumperrunner/thumper.go index 4efafaa..cb1e634 100644 --- a/internal/thumperrunner/thumper.go +++ b/internal/thumperrunner/thumper.go @@ -33,6 +33,7 @@ func RunWorker(options WorkerOptions) { script: script, client: options.Client, numExecuted: numExecuted, + source: script.source, } choices = append(choices, weightedrand.NewChoice(executable, script.weight)) } From cbed6583297bf184a5260c311428a1d6b1e7a22d Mon Sep 17 00:00:00 2001 From: Stefano Scafiti Date: Tue, 23 Jun 2026 14:47:46 +0200 Subject: [PATCH 2/2] Add nightly image build on push to main Add a nightly workflow that builds and pushes thumper images on every push to main, mirroring SpiceDB's nightly setup but adapted to thumper's existing release pipeline (no snaps, ko-based multi-arch, no buildx). - .github/workflows/nightly.yaml: push-to-main trigger running goreleaser with the nightly config. - .goreleaser.nightly.yaml: builds linux amd64/arm64 images to the existing authzed / ghcr.io/authzed / quay.io/authzed repos, tagged only with a commit-based version (no "latest") so release images are never overwritten. --- .github/workflows/nightly.yaml | 35 +++++++++++++++++++++++ .goreleaser.nightly.yaml | 52 ++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 .github/workflows/nightly.yaml create mode 100644 .goreleaser.nightly.yaml diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml new file mode 100644 index 0000000..6989bc0 --- /dev/null +++ b/.github/workflows/nightly.yaml @@ -0,0 +1,35 @@ +--- +name: "nightly" +on: # yamllint disable-line rule:truthy + push: + branches: + - "main" +permissions: + contents: "read" + +jobs: + nightly: + runs-on: "ubuntu-latest" + permissions: + contents: "write" + packages: "write" # publish to GHCR + steps: + - uses: "actions/checkout@v6" + with: + fetch-depth: 0 + - uses: "authzed/actions/setup-go@main" + - uses: "authzed/actions/docker-login@main" + with: + quayio_token: "${{ secrets.QUAYIO_PASSWORD }}" + github_token: "${{ secrets.GITHUB_TOKEN }}" + dockerhub_token: "${{ secrets.DOCKERHUB_ACCESS_TOKEN }}" + - uses: "goreleaser/goreleaser-action@v6" + with: + distribution: "goreleaser-pro" + # NOTE: keep in sync with goreleaser version in the release job. + # github actions don't allow yaml anchors. + version: "2.4.8" + args: "release -f .goreleaser.nightly.yaml --clean --nightly" + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + GORELEASER_KEY: "${{ secrets.GORELEASER_KEY }}" diff --git a/.goreleaser.nightly.yaml b/.goreleaser.nightly.yaml new file mode 100644 index 0000000..d6d57c2 --- /dev/null +++ b/.goreleaser.nightly.yaml @@ -0,0 +1,52 @@ +# This config builds nightly ("devel") images on every push to main. It mirrors +# .goreleaser.yaml but builds linux images only, omits the "latest" tag so that +# release images are never overwritten, and derives a commit-based version. +# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json + +--- +version: 2 + +builds: + - id: "main-build" + env: + - "CGO_ENABLED=0" + goos: + - "linux" + goarch: + - "amd64" + - "arm64" + main: "./cmd/thumper/" + dir: "." + mod_timestamp: "{{ .CommitTimestamp }}" + +kos: + - &ko-defaults + id: "dockerhub-ko" + build: "main-build" + platforms: + - "linux/amd64" + - "linux/arm64" + repository: "authzed" + tags: + - "v{{ .Version }}" + creation_time: "{{ .CommitTimestamp }}" + ko_data_creation_time: "{{ .CommitTimestamp }}" + sbom: "none" + # This prevents it from adding the md5sum after the name of + # the image + base_import_paths: true + - <<: *ko-defaults + id: "ghcr-build" + repository: "ghcr.io/authzed" + - <<: *ko-defaults + id: "quay-build" + repository: "quay.io/authzed" + +checksum: + name_template: "checksums.txt" + +# With --nightly, the version (and therefore the image tag) becomes +# "-", e.g. v0.1.1-1874ebe, which is unique per commit +# and never collides with a real release tag. +nightly: + version_template: "{{ incpatch .Version }}-{{ .ShortCommit }}"