Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 112 additions & 8 deletions .github/workflows/scenario-dev.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
name: scenario-dev
run-name: scenario-dev / ${{ inputs.scenario || 'posthog_frozen_perf' }} / Trino ${{ inputs.trino_perf_shape || 'baseline' }}

on:
workflow_dispatch:
Expand All @@ -13,6 +14,17 @@ on:
required: false
default: ""
type: string
trino_perf_shape:
description: Trino execution shape (experiments require posthog_frozen_perf)
required: false
default: baseline
type: choice
options:
- baseline
- large
- scaleout
- large-scaleout
- all
schedule:
- cron: "17 8 * * *"

Expand All @@ -25,7 +37,24 @@ concurrency:
cancel-in-progress: false

jobs:
plan:
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
matrix: ${{ steps.shapes.outputs.matrix }}
env:
SCENARIO_NAME: ${{ github.event_name == 'schedule' && 'posthog_frozen_perf' || inputs.scenario }}
TRINO_PERF_SHAPE: ${{ inputs.trino_perf_shape || 'baseline' }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Select Trino shapes
id: shapes
run: |
matrix="$(bash scripts/scenario_trino_shapes.sh)"
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"

scenario-runner-image:
needs: [plan]
uses: ./.github/workflows/_image-build.yml
with:
dockerfile: tests/mw-dev/scenario/Dockerfile
Expand All @@ -37,6 +66,7 @@ jobs:
ecr-role: ${{ vars.AWS_ECR_PRS_PUBLISH_IAM_ROLE }}

duckgres-image:
needs: [plan]
if: ${{ github.event_name != 'workflow_dispatch' || inputs.duckgres_image == '' }}
uses: ./.github/workflows/_image-build.yml
with:
Expand All @@ -57,12 +87,20 @@ jobs:
ecr-role: ${{ vars.AWS_ECR_PRS_PUBLISH_IAM_ROLE }}

scenario:
needs: [scenario-runner-image, duckgres-image]
if: ${{ always() && needs.scenario-runner-image.result == 'success' && (needs.duckgres-image.result == 'success' || (github.event_name == 'workflow_dispatch' && inputs.duckgres_image != '')) }}
name: scenario (${{ matrix.shape }})
needs: [plan, scenario-runner-image, duckgres-image]
if: ${{ always() && needs.plan.result == 'success' && needs.scenario-runner-image.result == 'success' && (needs.duckgres-image.result == 'success' || (github.event_name == 'workflow_dispatch' && inputs.duckgres_image != '')) }}
strategy:
# Each job includes teardown. A fresh runner also refreshes credentials
# and the timeout budget before admitting the next shape.
max-parallel: 1
fail-fast: false
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
runs-on: ubuntu-24.04
timeout-minutes: 270
env:
SCENARIO_NAME: ${{ github.event_name == 'schedule' && 'posthog_frozen_perf' || inputs.scenario }}
TRINO_PERF_SHAPE: ${{ matrix.shape }}
SCENARIO_RUNNER_IMAGE: ${{ needs.scenario-runner-image.outputs.image }}
WORKER_IMAGE: ${{ (github.event_name == 'workflow_dispatch' && inputs.duckgres_image) || needs.duckgres-image.outputs.image }}
CONTROLPLANE_IMAGE: ${{ (github.event_name == 'workflow_dispatch' && inputs.duckgres_image) || needs.duckgres-image.outputs.image }}
Expand All @@ -76,8 +114,8 @@ jobs:
TRINO_POD_IDENTITY_ROLE: ${{ secrets.MW_DEV_TRINO_POD_IDENTITY_ROLE }}
TRINO_IMAGE: ghcr.io/posthog/trino:4505364c570d6b51edecd299b603fca4b6693d86@sha256:ac80c275fd18a439d25da5652ab5cd3c80bcbdd2d88d64c9722dc3e8bb68ba07
E2E_SUITE: ${{ (github.event_name == 'schedule' || inputs.scenario == 'posthog_frozen_perf') && 'trino' || 'neutral' }}
PR_NUMBER: ${{ github.run_id }}
NAMESPACE: duckgres-ci-pr-${{ github.run_id }}
PR_NUMBER: ${{ github.run_id }}${{ matrix.suffix }}
NAMESPACE: duckgres-ci-pr-${{ github.run_id }}${{ matrix.suffix }}
DUCKGRES_SCENARIO_MAX_RUNTIME: 4h
DUCKGRES_SCENARIO_GO_TEST_TIMEOUT: 4h15m
# Add process headroom for repeated full-dataset pgwire aggregates.
Expand All @@ -95,7 +133,7 @@ jobs:
go-version-file: go.mod

- name: Test scenario workflow scripts
run: go test -count=1 ./tests/mw-dev/scenario ./tests/mw-dev ./tests/perf/publishercli
run: go test -count=1 ./tests/mw-dev/scenario ./tests/mw-dev ./tests/perf/publishercli ./tests/perf/shapecompare ./cmd/duckgres-perf-shape-summary

- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
Expand All @@ -120,19 +158,32 @@ jobs:
run: aws eks update-kubeconfig --name "$CLUSTER_NAME" --region "$AWS_REGION" --alias "$KUBE_CONTEXT"

- name: Load Athena perf configuration
if: env.SCENARIO_NAME == 'posthog_frozen_perf'
if: env.SCENARIO_NAME == 'posthog_frozen_perf' && matrix.shape == 'baseline'
run: bash scripts/scenario_athena_config.sh >> "$GITHUB_ENV"

- name: Deploy isolated Duckgres stack
id: deploy
run: tests/mw-dev/run.sh deploy

- name: Run selected scenario
id: benchmark
run: tests/mw-dev/run.sh test-scenario

- name: Publish scenario summary
if: always()
run: |
set -euo pipefail
shape_file=artifacts/scenario-dev/trino-perf-shape.json
if [ -f "$shape_file" ]; then
{
echo '## Trino execution shape'
echo
echo '```json'
cat "$shape_file"
echo '```'
echo
} >> "$GITHUB_STEP_SUMMARY"
fi
summary_file="$(find artifacts/scenario-dev -type f -name scenario_summary.md -print -quit 2>/dev/null || true)"
if [ -n "$summary_file" ]; then
cat "$summary_file" >> "$GITHUB_STEP_SUMMARY"
Expand All @@ -149,20 +200,36 @@ jobs:
run: tests/mw-dev/run.sh diagnostics

- name: Teardown
id: teardown
if: always()
run: tests/mw-dev/run.sh teardown

- name: Record shape outcome
if: always()
env:
DEPLOY_OUTCOME: ${{ steps.deploy.outcome }}
SCENARIO_OUTCOME: ${{ steps.benchmark.outcome }}
TEARDOWN_OUTCOME: ${{ steps.teardown.outcome }}
run: |
mkdir -p artifacts/scenario-dev
jq -n --arg shape "$TRINO_PERF_SHAPE" \
--arg deploy "$DEPLOY_OUTCOME" --arg scenario "$SCENARIO_OUTCOME" \
--arg teardown "$TEARDOWN_OUTCOME" \
'$ARGS.named' > artifacts/scenario-dev/shape-result.json

- name: Upload scenario artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: scenario-dev-${{ github.run_id }}-${{ github.run_attempt }}
name: scenario-dev-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.shape }}
path: artifacts/scenario-dev/
if-no-files-found: warn
retention-days: 14

- name: Publish scenario perf results
if: ${{ always() && github.ref == 'refs/heads/main' }}
# Shape experiments remain downloadable artifacts; publishing them
# would mix different resource budgets into the daily baseline.
if: ${{ always() && github.ref == 'refs/heads/main' && env.TRINO_PERF_SHAPE == 'baseline' && inputs.trino_perf_shape != 'all' }}
timeout-minutes: 10
env:
MW_DEV_SCENARIO_PERF_SECRET_ID: ${{ vars.MW_DEV_SCENARIO_PERF_SECRET_ID }}
Expand Down Expand Up @@ -195,3 +262,40 @@ jobs:
fi
done
exit "$publish_failed"

compare-shapes:
needs: [plan, scenario]
if: ${{ always() && needs.plan.result == 'success' && inputs.trino_perf_shape == 'all' }}
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
- name: Download shape artifacts
# Missing uploads are reported as missing shapes by the comparison.
continue-on-error: true
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: scenario-dev-${{ github.run_id }}-${{ github.run_attempt }}-*
path: artifacts/trino-shapes
merge-multiple: false
- name: Compare Trino shapes
run: go run ./cmd/duckgres-perf-shape-summary --artifacts-dir artifacts/trino-shapes > trino-shape-comparison.md
- name: Publish Trino shape comparison
if: always()
run: |
if [ -s trino-shape-comparison.md ]; then
cat trino-shape-comparison.md >> "$GITHUB_STEP_SUMMARY"
else
echo 'Trino shape comparison unavailable. Inspect the shape jobs and their artifacts.' >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload Trino shape comparison
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: trino-shape-comparison-${{ github.run_id }}-${{ github.run_attempt }}
path: trino-shape-comparison.md
if-no-files-found: warn
retention-days: 14
33 changes: 33 additions & 0 deletions cmd/duckgres-perf-shape-summary/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package main

import (
"flag"
"fmt"
"io"
"os"

"github.com/posthog/duckgres/tests/perf/shapecompare"
)

func main() {
if err := run(os.Args[1:], os.Stdout); err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

func run(args []string, stdout io.Writer) error {
flags := flag.NewFlagSet("duckgres-perf-shape-summary", flag.ContinueOnError)
dir := flags.String("artifacts-dir", "", "required directory containing one artifact directory per Trino shape")
if err := flags.Parse(args); err != nil {
return err
}
if *dir == "" || flags.NArg() != 0 {
return fmt.Errorf("usage: duckgres-perf-shape-summary --artifacts-dir <directory>")
}
report, err := shapecompare.Generate(*dir)
if _, writeErr := io.WriteString(stdout, report); writeErr != nil {
return writeErr
}
return err
}
33 changes: 33 additions & 0 deletions cmd/duckgres-perf-shape-summary/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package main

import (
"bytes"
"errors"
"strings"
"testing"
)

func TestRunRequiresArtifactsDirectory(t *testing.T) {
for _, args := range [][]string{nil, {"--artifacts-dir", "test", "extra"}} {
if err := run(args, &bytes.Buffer{}); err == nil {
t.Fatal("expected usage error")
}
}
}

func TestRunPublishesIncompleteReportBeforeFailing(t *testing.T) {
var out bytes.Buffer
err := run([]string{"--artifacts-dir", t.TempDir()}, &out)
if err == nil || !strings.Contains(out.String(), "Comparison incomplete") || !strings.Contains(out.String(), "missing artifact") {
t.Fatalf("report=%s, error=%v", out.String(), err)
}
}

type failedWriter struct{}

func (failedWriter) Write([]byte) (int, error) { return 0, errors.New("write failed") }
func TestRunReturnsWriteFailure(t *testing.T) {
if err := run([]string{"--artifacts-dir", t.TempDir()}, failedWriter{}); err == nil || err.Error() != "write failed" {
t.Fatalf("got %v", err)
}
}
1 change: 1 addition & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ test:
test-unit:
go test -v -p 1 . ./configresolve/... ./duckdbservice/... ./server/... ./transpiler/... ./internal/... ./tests/manifests/...
go test -v -count=1 ./tests/mw-dev/...
go test -v -count=1 ./tests/perf/shapecompare ./cmd/duckgres-perf-shape-summary

# Run scenario runner unit tests
[group('test')]
Expand Down
29 changes: 29 additions & 0 deletions scripts/scenario_run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Optional environment:
DUCKGRES_SCENARIO_DBT_BIN
DUCKGRES_SCENARIO_MAX_RUNTIME
DUCKGRES_SCENARIO_GO_TEST_TIMEOUT
DUCKGRES_SCENARIO_PERF_MODE (default: full; trino-only for posthog-frozen-perf)

Scenario-specific required environment:
DUCKGRES_SCENARIO_ORG_ID (required by successful provisioning scenarios)
Expand Down Expand Up @@ -83,6 +84,29 @@ root_relative_path() {
scenario_file="$(root_relative_path "$scenario_file")"
output_base="$(root_relative_path "$output_base")"

# Keep preflight aligned with the Go scenario loader: Trino-only removes the
# Athena target and its environment requirements before template resolution.
perf_mode="${DUCKGRES_SCENARIO_PERF_MODE:-full}"
case "$perf_mode" in
full) ;;
trino-only)
scenario_name="$(awk '/^name:[[:space:]]*/ {
sub(/^name:[[:space:]]*/, "")
sub(/[[:space:]]*$/, "")
gsub(/^["'\'']|["'\'']$/, "")
print; exit
}' "$scenario_file")"
if [ "$scenario_name" != posthog-frozen-perf ]; then
echo "DUCKGRES_SCENARIO_PERF_MODE=trino-only requires the posthog-frozen-perf scenario." >&2
exit 2
fi
;;
*)
echo "DUCKGRES_SCENARIO_PERF_MODE must be full or trino-only." >&2
exit 2
;;
esac

scenario_required_env() {
awk '
/^[^[:space:]]/ { in_required = 0 }
Expand All @@ -104,6 +128,11 @@ required=(
)
if [ -f "$scenario_file" ]; then
while IFS= read -r name; do
if [ "$perf_mode" = trino-only ]; then
case "$name" in
DUCKGRES_SCENARIO_ATHENA_REGION|DUCKGRES_SCENARIO_ATHENA_WORKGROUP|DUCKGRES_SCENARIO_ATHENA_DATABASE|DUCKGRES_SCENARIO_ATHENA_RESULTS_S3_URI) continue ;;
esac
fi
required+=("$name")
done < <(scenario_required_env "$scenario_file")
elif [ "$check_env_only" -eq 1 ]; then
Expand Down
24 changes: 24 additions & 0 deletions scripts/scenario_trino_shapes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Print the scenario workflow matrix. Single-shape runs retain the original run
# ID; all-shape runs append distinct numeric suffixes for harness isolation.
set -euo pipefail

shape="${TRINO_PERF_SHAPE:-baseline}"
case "$shape" in
baseline|large|scaleout|large-scaleout|all) ;;
*)
printf '%s\n' 'Invalid TRINO_PERF_SHAPE: expected baseline, large, scaleout, large-scaleout, or all' >&2
exit 1
;;
esac

if [[ "$shape" != baseline && "${SCENARIO_NAME:-}" != posthog_frozen_perf ]]; then
printf '%s\n' 'Trino shape experiments require posthog_frozen_perf' >&2
exit 1
fi

if [[ "$shape" == all ]]; then
printf '%s\n' '{"include":[{"shape":"baseline","suffix":"1"},{"shape":"large","suffix":"2"},{"shape":"scaleout","suffix":"3"},{"shape":"large-scaleout","suffix":"4"}]}'
else
printf '{"include":[{"shape":"%s","suffix":""}]}\n' "$shape"
fi
14 changes: 14 additions & 0 deletions tests/mw-dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ The isolated control plane's default worker request is configurable through
`scenario-dev.yml` explicitly overrides them to 3 CPU and 12Gi for the frozen
perf workload. Direct `run.sh` callers can make the same explicit override.

Manual frozen-perf runs can select `TRINO_PERF_SHAPE=baseline|large|scaleout|large-scaleout`
(`trino_perf_shape` in workflow dispatch). The default remains `baseline` for
scheduled runs and the Trino E2E lane. Workflow dispatch also accepts `all` to
run all four shapes sequentially with shared image builds and one comparison
summary. Each shape receives a separate temporary stack and cleanup. Baseline
runs the full cross-engine benchmark; nonbaseline shapes measure only Trino,
with identical SQL, warmup, and measured iterations, while retaining setup and
validation. The harness derives `DUCKGRES_SCENARIO_PERF_MODE=full` or
`trino-only` from the shape and records it in provenance; an inherited value
cannot override it. Nonbaseline shapes need no Athena configuration or scenario
Pod Identity role. See the
[Trino experiment runbook](../perf/README.md#trino-worker-shape-experiments)
for resource budgets, result provenance, sequential comparison, and recovery.

### Scenario Trino readiness

Scenarios that opt an org into Trino in their `provision_warehouse` request can
Expand Down
Loading
Loading