Skip to content
Merged
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
2 changes: 2 additions & 0 deletions docs/perf-harness-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
Golden-query performance signal collection for `pgwire` and `flight` protocols.
This is observability-only; there is no pass/fail performance gate.

Status: the old prod-us deployed perf runner path is legacy while the dev Duckgres Scenario Runner replacement is being built. Prefer `tests/scenario/scenarios/posthog_frozen_metadata.yaml` for current frozen dataset end-to-end validation; keep this perf harness for local/library use and historical artifact compatibility until the scenario perf adapter is available.

## Local Prerequisites

- Run commands from the Duckgres repository root.
Expand Down
23 changes: 23 additions & 0 deletions docs/runbooks/scenario-runner.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

The scenario runner executes end-to-end managed-warehouse flows against a configured dev environment. The first smoke scenario provisions a warehouse, waits for readiness, runs `SELECT 1` over PGWire with managed-hostname SNI, then deprovisions and verifies cleanup.

The frozen metadata scenario provisions the same fresh dev warehouse shape, creates read-only views over frozen persons/events parquet supplied by `DUCKGRES_SCENARIO_FROZEN_S3_URI`, validates the dataset manifest row, runs lightweight metadata/user-exploration queries, then deprovisions.

## Required Environment

Set these before running a real scenario:
Expand All @@ -28,6 +30,12 @@ export DUCKGRES_SCENARIO_MAX_RUNTIME="30m"
export DUCKGRES_SCENARIO_GO_TEST_TIMEOUT="60m"
```

Frozen dataset scenarios additionally require:

```bash
export DUCKGRES_SCENARIO_FROZEN_S3_URI="s3://<dev-managed-bucket>/frozen_v1/"
```

Do not commit concrete dev endpoints, secrets, org IDs, or private bucket names.

## Run
Expand All @@ -44,6 +52,12 @@ Run the dev smoke:
just scenario-smoke
```

Run frozen metadata exploration:

```bash
just scenario-frozen-metadata
```

Run a specific scenario file:

```bash
Expand All @@ -52,6 +66,15 @@ just scenario scenario=tests/scenario/scenarios/provision_smoke.yaml

Artifacts are written under `artifacts/scenario/<run_id>/`.

The frozen metadata scenario uses:

- `tests/scenario/scenarios/posthog_frozen_metadata.yaml`
- `tests/scenario/sql/setup_frozen_views.sql`
- `tests/scenario/sql/metadata_catalog.yaml`

`DUCKGRES_SCENARIO_FROZEN_S3_URI` must point at a dev-owned frozen dataset prefix with `persons/` and `events/` parquet children.
The provisioned Duckgres worker role also needs read/list access to that prefix; the runner process only supplies the URI, while the worker performs the S3 reads during `read_parquet`.

## Leaked Dev Warehouse Recovery

The smoke scenario has an `always_run` deprovision step, but an interrupted process can still leave dev resources behind. To clean up:
Expand Down
5 changes: 5 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,11 @@ scenario scenario="tests/scenario/scenarios/provision_smoke.yaml":
scenario-smoke:
./scripts/scenario_run.sh tests/scenario/scenarios/provision_smoke.yaml

# Run the dev frozen dataset metadata exploration scenario
[group('test')]
scenario-frozen-metadata:
./scripts/scenario_run.sh tests/scenario/scenarios/posthog_frozen_metadata.yaml

# Lint (matches CI — uses golangci-lint, not go vet)
[group('test')]
lint:
Expand Down
14 changes: 14 additions & 0 deletions scripts/scenario_run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Optional environment:
DUCKGRES_SCENARIO_PG_CONNECT_TIMEOUT
DUCKGRES_SCENARIO_MAX_RUNTIME
DUCKGRES_SCENARIO_GO_TEST_TIMEOUT
DUCKGRES_SCENARIO_FROZEN_S3_URI (required by frozen dataset scenarios)
USAGE
}

Expand Down Expand Up @@ -64,6 +65,16 @@ while [ "$#" -gt 0 ]; do
esac
done

script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd -- "$script_dir/.." && pwd)"

root_relative_path() {
case "$1" in
/*) printf '%s\n' "$1" ;;
*) printf '%s\n' "$repo_root/$1" ;;
esac
}

required=(
DUCKGRES_SCENARIO_API_BASE
DUCKGRES_SCENARIO_INTERNAL_SECRET
Expand All @@ -90,6 +101,9 @@ if [ "$check_env_only" -eq 1 ]; then
exit 0
fi

scenario_file="$(root_relative_path "$scenario_file")"
output_base="$(root_relative_path "$output_base")"

args=(
go test -count=1 ./tests/scenario
-timeout "$go_test_timeout"
Expand Down
16 changes: 13 additions & 3 deletions tests/scenario/core/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import (
)

type Scenario struct {
Name string `yaml:"name" json:"name"`
RunIDPrefix string `yaml:"run_id_prefix" json:"run_id_prefix,omitempty"`
Steps []Step `yaml:"steps" json:"steps"`
Name string `yaml:"name" json:"name"`
RunIDPrefix string `yaml:"run_id_prefix" json:"run_id_prefix,omitempty"`
RequiredEnv []string `yaml:"required_env" json:"required_env,omitempty"`
Steps []Step `yaml:"steps" json:"steps"`
}

type Step struct {
Expand All @@ -26,6 +27,7 @@ type Step struct {
type rawScenario struct {
Name string `yaml:"name"`
RunIDPrefix string `yaml:"run_id_prefix"`
RequiredEnv []string `yaml:"required_env"`
Steps []rawStep `yaml:"steps"`
}

Expand Down Expand Up @@ -59,6 +61,7 @@ func normalizeScenario(raw rawScenario) (Scenario, error) {
scenario := Scenario{
Name: strings.TrimSpace(raw.Name),
RunIDPrefix: strings.TrimSpace(raw.RunIDPrefix),
RequiredEnv: make([]string, 0, len(raw.RequiredEnv)),
Steps: make([]Step, 0, len(raw.Steps)),
}
if scenario.Name == "" {
Expand All @@ -67,6 +70,13 @@ func normalizeScenario(raw rawScenario) (Scenario, error) {
if len(raw.Steps) == 0 {
return Scenario{}, fmt.Errorf("scenario steps must include at least one step")
}
for i, key := range raw.RequiredEnv {
key = strings.TrimSpace(key)
if key == "" {
return Scenario{}, fmt.Errorf("required_env[%d] must be non-empty", i)
}
scenario.RequiredEnv = append(scenario.RequiredEnv, key)
}

seen := make(map[string]struct{}, len(raw.Steps))
var previousID string
Expand Down
23 changes: 23 additions & 0 deletions tests/scenario/core/catalog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ steps:
func TestParseScenarioAllowsOpenWithMap(t *testing.T) {
raw := []byte(`
name: with-map
required_env:
- DUCKGRES_SCENARIO_FROZEN_S3_URI
steps:
- id: query
type: fake
Expand All @@ -158,7 +160,28 @@ steps:
if err != nil {
t.Fatalf("ParseScenario returned error: %v", err)
}
if got := scenario.RequiredEnv; len(got) != 1 || got[0] != "DUCKGRES_SCENARIO_FROZEN_S3_URI" {
t.Fatalf("required_env = %#v, want frozen S3 URI", got)
}
if scenario.Steps[0].With["sql"] != "SELECT 1" {
t.Fatalf("unexpected with map: %#v", scenario.Steps[0].With)
}
}

func TestParseScenarioRejectsEmptyRequiredEnv(t *testing.T) {
_, err := ParseScenario([]byte(`
name: bad-env
required_env: [DUCKGRES_SCENARIO_FROZEN_S3_URI, ""]
steps:
- id: query
type: fake
with:
sql: SELECT 1
`))
if err == nil {
t.Fatal("expected empty required_env entry to fail")
}
if !strings.Contains(err.Error(), "required_env[1] must be non-empty") {
t.Fatalf("error = %v, want required_env index", err)
}
}
29 changes: 29 additions & 0 deletions tests/scenario/core/templates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package core

import (
"fmt"
"os"
"regexp"
"strings"
)

var envTemplatePattern = regexp.MustCompile(`\$\{env:([A-Za-z_][A-Za-z0-9_]*)\}`)

// ResolveEnvTemplates replaces ${env:NAME} placeholders without exposing values in errors.
func ResolveEnvTemplates(text string) (string, error) {
var missing []string
out := envTemplatePattern.ReplaceAllStringFunc(text, func(match string) string {
parts := envTemplatePattern.FindStringSubmatch(match)
key := parts[1]
value := os.Getenv(key)
if value == "" {
missing = append(missing, key)
return match
}
return value
})
if len(missing) != 0 {
return "", fmt.Errorf("missing required env template value(s): %s", strings.Join(missing, ", "))
}
return out, nil
}
Loading
Loading