Skip to content
Open
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
87 changes: 87 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Stroppy - Project Context

Database stress testing CLI tool powered by k6 workload engine. Apache 2.0 licensed.

## Architecture Overview

Stroppy is a **k6 extension** (`k6/x/stroppy`) that adds database-specific capabilities to k6's load testing engine. Test scripts are written in TypeScript, transpiled via esbuild, and executed inside k6's Sobek JavaScript runtime.

### Binary Layout

- `stroppy` CLI wraps k6 with convenience commands (`gen`, `run`, `version`)
- `k6` binary is also built with the stroppy extension embedded
- Users can use either `stroppy run <script.ts>` or `./build/k6 run <script.ts>`

### Core Components

| Component | Path | Purpose |
|-----------|------|---------|
| CLI commands | `cmd/stroppy/commands/` | `gen`, `run`, `version` subcommands via cobra |
| k6 module | `cmd/xk6air/` | Registers `k6/x/stroppy` module, manages per-VU driver/generator instances |
| Driver interface | `pkg/driver/dispatcher.go` | Registry pattern: `RegisterDriver()` + `Dispatch()` |
| PostgreSQL driver | `pkg/driver/postgres/` | pgxpool-based, supports PLAIN_QUERY and COPY_FROM insertion |
| Data generators | `pkg/common/generate/` | Uniform, Normal, Zipfian distributions; int/float/string/uuid/bool/datetime |
| TypeScript framework | `internal/static/` | `helpers.ts` (R/S/AB/DriverX), `parse_sql.ts`, generated type bindings |
| Script runner | `internal/runner/` | esbuild transpilation, config extraction via Sobek, k6 process management |
| Schema definitions | `proto/stroppy/` | config, descriptor, common, runtime, cloud schemas |
| Built-in workloads | `workloads/` | simple, tpcb, tpcc, tpcds presets |

### Driver System

Drivers register themselves via `init()` using `driver.RegisterDriver()`. The dispatcher looks up the constructor by `DriverConfig_DriverType` enum. To add a new driver:

1. Create package under `pkg/driver/<name>/`
2. Implement `driver.Driver` interface (InsertValues, RunQuery, Teardown, Configure)
3. Call `driver.RegisterDriver()` in `init()`
4. Import the package in `cmd/xk6air/module.go` for side-effect registration

### TypeScript API (helpers.ts)

- `R` - Random generators: `R.str()`, `R.int32()`, `R.float()`, `R.double()`, `R.bool()`, `R.datetimeConst()`
- `S` - Sequence (unique) generators: `S.str()`, `S.int32()`
- `AB` - Alphabets: `en`, `enNum`, `num`, `enUpper`, `enSpc`, `enNumSpc`
- `DriverX` - Typed driver wrapper with metrics tracking
- `Step()` - Named execution blocks with cloud notification
- `NewGen()` / `NewGroupGen()` - Low-level generator creation

### SQL Syntax

- Query parameters use `:paramName` syntax, converted to PostgreSQL `$1, $2...` placeholders
- SQL files support structured parsing:
- `--+ section_name` groups SQL statements into sections
- `--= query_name` names individual queries within sections
- `parse_sql_with_groups()` returns `Record<string, ParsedQuery[]>`

### Build System

- `make build` - Builds k6 with xk6air extension via xk6
- `make proto` - Generates Go, TypeScript, gRPC, docs from proto files
- `make install-bin-deps` - Installs protoc plugins, xk6, esbuild, etc.
- Go 1.24.3+, Node.js required for full build

### Key Dependencies

- go.k6.io/k6 v1.6.0 (load testing engine)
- github.com/jackc/pgx/v5 (PostgreSQL driver)
- github.com/grafana/sobek (JavaScript engine for config extraction)
- github.com/spf13/cobra (CLI framework)
- connectrpc.com/connect (gRPC)
- OpenTelemetry SDKs (metrics export)

### K6 Integration

- k6 web dashboard: `K6_WEB_DASHBOARD=true` enables real-time dashboard
- HTML report export: `K6_WEB_DASHBOARD_EXPORT=report.html`
- All k6 CLI flags pass through after `--` separator: `stroppy run script.ts -- --vus 10 --duration 30s`
- k6 scenarios, thresholds, and metrics all work natively

### Docker

- Image: `ghcr.io/stroppy-io/stroppy:latest`
- Built-in workloads available at `/workloads/` inside container
- `DRIVER_URL` env var for database connection
- `--network host` for localhost database access

### Documentation Site

Docusaurus-based docs live in the GitHub Pages site at `stroppy-io.github.io`.
48 changes: 45 additions & 3 deletions cmd/stroppy/commands/root.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,71 @@
package commands

import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"runtime/debug"

"github.com/spf13/cobra"
"go.k6.io/k6/cmd/state"
"go.uber.org/zap"

"github.com/stroppy-io/stroppy/cmd/stroppy/commands/gen"
"github.com/stroppy-io/stroppy/cmd/stroppy/commands/run"
"github.com/stroppy-io/stroppy/internal/version"
"github.com/stroppy-io/stroppy/pkg/common/logger"
)

var rootCmd = &cobra.Command{
Use: "stroppy",
Short: "Tool to generate and run stress tests (e.g benchmarking) for databases",
}

// versionJSON controls whether `stroppy version` outputs machine-readable JSON.
// When more component versions are added (k6, drivers, etc.), --json gives
// programmatic consumers a stable format to parse instead of scraping text lines.
var versionJSON bool

var versionCmd = &cobra.Command{
Use: "version",
Short: "Print versions of stroppy components",
Long: ``,
Run: func(_ *cobra.Command, _ []string) {
logger.Info("Stroppy version", zap.String("version", version.Version))
versions := map[string]string{
"stroppy": version.Version,
}

// Pull dependency versions from the compiled binary's module info.
// These stay in sync with go.mod automatically — no hardcoding.
if info, ok := debug.ReadBuildInfo(); ok {
for _, dep := range info.Deps {
switch dep.Path {
case "go.k6.io/k6":
versions["k6"] = dep.Version
case "github.com/jackc/pgx/v5":
versions["pgx"] = dep.Version
}
}
}

if versionJSON {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(versions); err != nil {
log.Fatal(err)
}
} else {
// Fixed order for readable output.
for _, kv := range []struct{ k, v string }{
{"stroppy", versions["stroppy"]},
{"k6", versions["k6"]},
{"pgx", versions["pgx"]},
} {
if kv.v != "" {
fmt.Fprintf(os.Stdout, "%-8s %s\n", kv.k, kv.v)
}
}
}
},
}

Expand Down Expand Up @@ -68,5 +109,6 @@ func init() {
rootCmd.CompletionOptions.HiddenDefaultCmd = true
rootCmd.SetVersionTemplate(`{{with .Name}}{{printf "%s " .}}{{end}}{{printf "%s" .Version}}`)

versionCmd.Flags().BoolVar(&versionJSON, "json", false, "output versions as JSON")
rootCmd.AddCommand(versionCmd, run.Cmd, gen.Cmd)
}
14 changes: 8 additions & 6 deletions workloads/tpcc/tpcc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,42 +10,44 @@ import { NewGen, AB, R, Step, DriverX, S } from "./helpers.ts";
import { parse_sql_with_groups } from "./parse_sql.js";

const DURATION = __ENV.DURATION || "1h";
const VUS_SCALE = +(__ENV.VUS_SCALE || 1);
export const options: Options = {
setupTimeout: "5m",
scenarios: {
new_order: {
executor: "constant-vus",
exec: "new_order",
vus: 44,
vus: Math.max(1, Math.round(44 * VUS_SCALE)),
duration: DURATION,
},
payments: {
executor: "constant-vus",
exec: "payments",
vus: 43,
vus: Math.max(1, Math.round(43 * VUS_SCALE)),
duration: DURATION,
},
order_status: {
executor: "constant-vus",
exec: "order_status",
vus: 4,
vus: Math.max(1, Math.round(4 * VUS_SCALE)),
duration: DURATION,
},
delivery: {
executor: "constant-vus",
exec: "delivery",
vus: 4,
vus: Math.max(1, Math.round(4 * VUS_SCALE)),
duration: DURATION,
},
stock_level: {
executor: "constant-vus",
exec: "stock_level",
vus: 4,
vus: Math.max(1, Math.round(4 * VUS_SCALE)),
duration: DURATION,
},
},
};
// TPCC Configuration Constants
const POOL_SIZE = +(__ENV.POOL_SIZE || 100);
const WAREHOUSES = +(__ENV.SCALE_FACTOR || __ENV.WAREHOUSES || 1);
const DISTRICTS_PER_WAREHOUSE = 10;
const CUSTOMERS_PER_DISTRICT = 3000;
Expand All @@ -61,7 +63,7 @@ const driver = DriverX.fromConfig({
driver: {
url: __ENV.DRIVER_URL || "postgres://postgres:postgres@localhost:5432",
driverType: DriverConfig_DriverType.DRIVER_TYPE_POSTGRES,
connectionType: { is: {oneofKind:"sharedPool", sharedPool: {sharedConnections: 100}}},
connectionType: { is: {oneofKind:"sharedPool", sharedPool: {sharedConnections: POOL_SIZE}}},
dbSpecific: {
fields: [],
},
Expand Down