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 }}" 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)) }