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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ Group lines under `Added` / `Changed` / `Fixed` / `Removed`. Append a PR link

## [Unreleased]

### Added

- Custom Go workloads can run as standalone Stroppy applications with the standard drivers, CLI, probe, version, reports, and programmatic runtime API. ([#183](https://github.com/stroppy-io/stroppy/pull/183))
- Every constructed workload run report is saved under `~/.stroppy/reports/`; history write failures warn without failing the run. ([#183](https://github.com/stroppy-io/stroppy/pull/183))

### Changed

- Go applications can depend on Stroppy v6 through the standard `github.com/stroppy-io/stroppy/v6` module path. ([#181](https://github.com/stroppy-io/stroppy/pull/181))
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ Human output and diagnostics remain on stderr, so requested stdout stays valid
JSON. See [run reports](docs/run-reports.md) for report fields, output behavior,
and workload payloads.

Custom Go workloads can also run as self-contained applications with `go run .`
or a normal built executable. See [standalone workloads](docs/standalone-workloads.md).

Use `stroppy help` to explore available topics:

```bash
Expand Down
45 changes: 25 additions & 20 deletions cmd/stroppy/commands/baseline/baseline.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,14 @@ type options struct {
var opts options

// Cmd is the `stroppy baseline` subcommand.
var Cmd = &cobra.Command{
Use: "baseline",
Short: "Measure stroppy's own performance on this machine",
Long: `Measure the stroppy ceiling on this machine: no database required.
var Cmd = NewCommand()

// NewCommand creates an independent baseline command.
func NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "baseline",
Short: "Measure stroppy's own performance on this machine",
Long: `Measure the stroppy ceiling on this machine: no database required.

Two tiers run the built-in baseline workload back to back:

Expand All @@ -91,35 +95,36 @@ absolute thresholds, and a versioned JSON report is saved under
The pg-noop server binary is resolved from an embedded copy (release builds),
the ~/.stroppy/bin/ cache, or the pinned GitHub release (downloaded with
consent). Use --server-path or STROPPY_PG_NOOP_PATH to supply it directly.`,
Example: `
Example: `
stroppy baseline # full two-tier run, ~20s
stroppy baseline --quick # 1s phases, smaller load
stroppy baseline --tiers noop # framework tier only, no server needed
stroppy baseline --json # machine-readable report on stdout
stroppy baseline --server-path ./pgnoop
`,
RunE: func(cmd *cobra.Command, _ []string) error {
return run(cmd.Context(), cmd.OutOrStdout())
},
}
RunE: func(cmd *cobra.Command, _ []string) error {
return run(cmd.Context(), cmd.OutOrStdout())
},
}

func init() {
Cmd.Flags().BoolVar(&opts.quick, "quick", false, "shorter phases and a smaller load")
Cmd.Flags().BoolVar(&opts.jsonOut, "json", false, "print the report as JSON")
Cmd.Flags().StringSliceVar(&opts.tiers, "tiers", []string{tierNoop, tierWire},
cmd.Flags().BoolVar(&opts.quick, "quick", false, "shorter phases and a smaller load")
cmd.Flags().BoolVar(&opts.jsonOut, "json", false, "print the report as JSON")
cmd.Flags().StringSliceVar(&opts.tiers, "tiers", []string{tierNoop, tierWire},
"tiers to run: noop, wire")
Cmd.Flags().IntVar(&opts.vus, "vus", runtime.GOMAXPROCS(0),
cmd.Flags().IntVar(&opts.vus, "vus", runtime.GOMAXPROCS(0),
"VU count for the parallel tx phase")
Cmd.Flags().DurationVar(&opts.duration, "duration", 0,
cmd.Flags().DurationVar(&opts.duration, "duration", 0,
"tx phase duration (default 3s, 1s with --quick)")
Cmd.Flags().Int64Var(&opts.rows, "rows", 0,
cmd.Flags().Int64Var(&opts.rows, "rows", 0,
"load rows (default 250000, 100000 with --quick)")
Cmd.Flags().StringVar(&opts.serverPath, "server-path", "",
cmd.Flags().StringVar(&opts.serverPath, "server-path", "",
"path to a pg-noop binary (env STROPPY_PG_NOOP_PATH)")
Cmd.Flags().StringVar(&opts.download, "download", "ask",
cmd.Flags().StringVar(&opts.download, "download", "ask",
"server download consent: ask, always, or never")
Cmd.Flags().BoolVar(&opts.noSave, "no-save", false,
cmd.Flags().BoolVar(&opts.noSave, "no-save", false,
"do not write the report to ~/.stroppy/baselines/")

return cmd
}

// runPlan is the validated shape of one baseline invocation.
Expand Down Expand Up @@ -197,7 +202,7 @@ func run(ctx context.Context, out io.Writer) error {

report := Report{
Schema: reportSchema,
Stroppy: version.Version,
Stroppy: version.Resolve(),
Time: time.Now().UTC(),
Host: hostInfo(),
}
Expand Down
64 changes: 36 additions & 28 deletions cmd/stroppy/commands/help/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package help
import (
"errors"
"fmt"
"os"
"io"
"strings"

"github.com/spf13/cobra"
Expand All @@ -28,41 +28,49 @@ func Register(t Topic) {
}

// Cmd is the cobra command for `stroppy help [topic]`.
var Cmd = &cobra.Command{
Use: "help [topic]",
Short: "Show help about a topic",
Long: `Show extended help about a topic. Run without arguments to list available topics.`,
// DisableFlagParsing so that topic names like "drivers" are not mistaken for flags.
Args: cobra.ArbitraryArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
printTopicList()

return nil
}
var Cmd = NewCommand()

// NewCommand creates an independent help command.
func NewCommand() *cobra.Command {
return &cobra.Command{
Use: "help [topic]",
Short: "Show help about a topic",
Long: `Show extended help about a topic. Run without arguments to list available topics.`,
Args: cobra.ArbitraryArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return printTopicList(cmd.OutOrStdout())
}

name := strings.ToLower(args[0])
for _, t := range topics {
if t.Name == name {
fmt.Fprint(os.Stdout, t.Long)
name := strings.ToLower(args[0])
for _, topic := range topics {
if topic.Name == name {
_, err := fmt.Fprint(cmd.OutOrStdout(), topic.Long)

return nil
return err
}
}
}

fmt.Fprintf(os.Stderr, "stroppy help: unknown topic %q\n\n", args[0])
printTopicList()
fmt.Fprintf(cmd.ErrOrStderr(), "stroppy help: unknown topic %q\n\n", args[0])
_ = printTopicList(cmd.OutOrStdout())

return fmt.Errorf("%w: %s", errUnknownTopic, args[0])
},
return fmt.Errorf("%w: %s", errUnknownTopic, args[0])
},
}
}

func printTopicList() {
fmt.Fprint(os.Stdout, "Available help topics:\n\n")
func printTopicList(output io.Writer) error {
if _, err := fmt.Fprint(output, "Available help topics:\n\n"); err != nil {
return err
}

for _, t := range topics {
fmt.Fprintf(os.Stdout, " %-20s %s\n", t.Name, t.Short)
for _, topic := range topics {
if _, err := fmt.Fprintf(output, " %-20s %s\n", topic.Name, topic.Short); err != nil {
return err
}
}

fmt.Fprint(os.Stdout, "\nUse 'stroppy help <topic>' for details.\n")
_, err := fmt.Fprint(output, "\nUse 'stroppy help <topic>' for details.\n")

return err
}
64 changes: 35 additions & 29 deletions cmd/stroppy/commands/probe/probe.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,40 +26,46 @@ var (
formats = []string{humanFormat, jsonFormat}
formatsWithCommas = strings.Join(formats, ", ")
ErrUnsoportedFormat = errors.New("unsupported format")
Cmd = func() *cobra.Command {
cmd := &cobra.Command{
Use: "probe",
Short: "List embedded workload presets and supported drivers",
Long: `Probe lists the embedded workload presets (their SQL dialects and docs)
Cmd = NewCommand(bench.RegisteredCatalog())
)

// NewCommand builds a probe command over an explicit workload catalog.
func NewCommand(workloadCatalog *bench.Catalog) *cobra.Command {
cmd := &cobra.Command{
Use: "probe",
Short: "List embedded workload presets and supported drivers",
Long: `Probe lists the embedded workload presets (their SQL dialects and docs)
and the insert methods each driver supports. Registered workload parameter schemas
are read without setting up a workload or connecting to a database.

-o json machine-readable output
`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
formatFlagValue := cmd.Flag(formatFlag).Value.String()

if !contains(formats, formatFlagValue) {
return fmt.Errorf(
"%q, available (%s): %w",
formatFlagValue,
formatsWithCommas,
ErrUnsoportedFormat,
)
}

return printCatalog(cmd.OutOrStdout(), formatFlagValue)
},
}
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
formatFlagValue := cmd.Flag(formatFlag).Value.String()

if !contains(formats, formatFlagValue) {
return fmt.Errorf(
"%q, available (%s): %w",
formatFlagValue,
formatsWithCommas,
ErrUnsoportedFormat,
)
}

cmd.Flags().
StringP(formatFlag, string(formatFlag[0]), humanFormat,
fmt.Sprintf("(%s)", formatsWithCommas))
return printCatalog(cmd.OutOrStdout(), workloadCatalog, formatFlagValue)
},
}

return cmd
}()
)
cmd.Flags().StringP(
formatFlag,
string(formatFlag[0]),
humanFormat,
fmt.Sprintf("(%s)", formatsWithCommas),
)

return cmd
}

func contains(haystack []string, needle string) bool {
for _, s := range haystack {
Expand All @@ -73,13 +79,13 @@ func contains(haystack []string, needle string) bool {

// printCatalog renders the embedded preset catalog, workload schemas, and driver
// insert-method matrix in the requested format.
func printCatalog(output io.Writer, format string) error {
func printCatalog(output io.Writer, workloadCatalog *bench.Catalog, format string) error {
catalog, err := workloads.Catalog()
if err != nil {
return fmt.Errorf("failed to build workloads catalog: %w", err)
}

descriptions, err := bench.DescribeAll()
descriptions, err := workloadCatalog.DescribeAll()
if err != nil {
return fmt.Errorf("failed to describe workloads: %w", err)
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/stroppy/commands/probe/probe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestJSONCatalogIncludesWorkloadSchemas(t *testing.T) {

func TestHumanCatalogIncludesGroupedWorkloads(t *testing.T) {
var output bytes.Buffer
if err := printCatalog(&output, humanFormat); err != nil {
if err := printCatalog(&output, bench.RegisteredCatalog(), humanFormat); err != nil {
t.Fatalf("printCatalog() error = %v", err)
}

Expand All @@ -50,7 +50,7 @@ func renderJSONCatalog(t *testing.T) []byte {
t.Helper()

var output bytes.Buffer
if err := printCatalog(&output, jsonFormat); err != nil {
if err := printCatalog(&output, bench.RegisteredCatalog(), jsonFormat); err != nil {
t.Fatalf("printCatalog() error = %v", err)
}

Expand Down
Loading
Loading