Skip to main content
Version: 6.0.0

Extensibility

Stroppy v6 extensions are Go source compiled into the binary. There is no runtime plugin or script loader. A product extension normally adds one of:

  • a registered bench.Workload;
  • SQL/JSON/README assets owned by that workload package;
  • a registered driver.Driver;
  • deterministic generation code under pkg/gen or a workload package.

Workload interface​

type Workload interface {
Name() string
Define(*Def) error
Setup(context.Context, *Bench) error
Iterate(context.Context, *Bench) error
Teardown(context.Context, *Bench) error
}

Lifecycle:

  1. Define declares typed parameters and captures resolved values.
  2. Setup runs once before virtual users start.
  3. Iterate runs according to selected executor.
  4. Teardown runs after completion or graceful cancellation.

Register a fresh factory from package init:

func init() {
bench.Register(func() bench.Workload { return &workload{} })
}

Factories must return a new non-nil workload. Names are global and unique.

Typed parameters​

Define declares each workload value once:

func (w *workload) Define(d *bench.Def) error {
w.scale = d.Param.Int("scale-factor", 1, "Number of partitions.").Value()
w.workers = d.Param.Int("load-workers", 1, "Load workers.").Value()
w.sqlFile = d.Param.String("sql-file", "", "SQL override.").Value()
return nil
}

Supported scalar types include string, boolean, integer, float, and duration. Each declaration becomes:

  • a --name CLI flag;
  • an uppercase environment input;
  • a lower-camel params config key;
  • a schema entry in stroppy probe -o json.

Shared executor parameters are declared by the engine under config run.

Steps and queries​

Wrap setup or workload phases with Bench.Step:

func (w *workload) Setup(ctx context.Context, b *bench.Bench) error {
if err := b.Step("create_schema", func() error {
return b.Exec(ctx, "CREATE TABLE events (id BIGINT PRIMARY KEY)", nil)
}); err != nil {
return err
}

return b.Step("load_data", func() error {
_, err := b.Insert(ctx, w.insertRequest())
return err
})
}

Use StepSilent("workload", ...) for per-iteration work to avoid log spam.

Bench query helpers include Exec, QueryRows, QueryRow, and QueryValue. They accept :named parameters through map[string]any.

Typed loads​

Construct a driver.InsertRequest with table, method, worker count, and gen.BatchSource:

return &driver.InsertRequest{
Table: "events",
Method: driver.InsertPlainBulk,
Workers: w.workers,
Source: source,
}

Use gen.SchemaBuilder plus gen.NewIndexedSource for index-addressable row formulas. Stateful canonical generators implement gen.BatchSource directly and provide partition seeking.

See SQL & Generators and the pinned v6 parallelism contract.

Workload-owned assets​

Asset-bearing packages embed their own files:

//go:embed *.sql README.md
var files embed.FS

func init() {
workloads.Register(workloads.PresetTPCB, files)
}

Add a preset constant/catalog entry when introducing a new asset namespace. Then blank-import its package in workloads/all/import.go so all built-ins register predictably.

Keep required filenames, sections, and named queries pinned with package contract tests. Existing workloads/internal/workloadtest helpers validate embedded files and SQL structure.

Driver interface​

type Driver interface {
Insert(context.Context, *InsertRequest) (*stats.Query, error)
RunQuery(context.Context, string, map[string]any) (*QueryResult, error)
Begin(context.Context, config.TxIsolationLevel) (Tx, error)
ClassifyError(error) ErrorFacts
Teardown(context.Context) error
}

Transactions return:

type Tx interface {
RunQuery(context.Context, string, map[string]any) (*QueryResult, error)
Commit(context.Context) error
Rollback(context.Context) error
Isolation() config.TxIsolationLevel
}

Constructor options include resolved driver config, logger, optional network dialer, and per-statement query timeout.

Adding a driver​

1. Add type and user config​

Add a config.DriverType constant, string mapping, and value enumeration under pkg/config. Extend strict config/schema tests and regenerate schema:

go generate ./pkg/config

2. Implement package​

Suggested layout:

pkg/driver/mydb/
├── driver.go
├── errors.go
├── dialect.go
├── insert.go
└── tx.go

SQL drivers can reuse pkg/driver/sqldriver for placeholder conversion, database/sql execution, rows normalization, bulk inserts, and teardown.

3. Register constructor​

func init() {
driver.RegisterDriver(config.DriverTypeMyDB, NewDriver)
}

Constructor shape:

func NewDriver(ctx context.Context, opts driver.Options) (driver.Driver, error)

4. Classify errors​

Translate backend-specific errors into driver.ErrorFacts. Do not choose retry or fatal behavior inside the driver; workloads own that policy.

Cover serialization, deadlock, lock timeout, transient, timeout, cancellation, and unsupported cases the backend can identify reliably. Unknown input should remain unknown.

5. Import package​

Add a blank import in cmd/stroppy/main.go so registration runs in the binary.

6. Advertise capabilities​

Update driver.InsertCapabilities and probe tests. Add a short preset in internal/runner/driver_preset.go only when useful.

Build and test​

Follow repository commands:

make build
make tests
make linter

Tagged integration requires the built binary and baseline services:

make tmpfs-up
make build
make integration
make tmpfs-down

Add package tests for registration, malformed input, capability resolution, cancellation, and backend error classification. Add integration coverage when behavior depends on a real database.

Reference implementations​

AreaSource
Minimal workloadworkloads/simple
Transactional workloadworkloads/tpcb
Stateful generator adapterpkg/datagen/tpchgen
PostgreSQL driverpkg/driver/postgres
Shared SQL driverpkg/driver/sqldriver
Sink driverpkg/driver/csv
Driver registrypkg/driver/dispatcher.go