Skip to content

[RNDSTROPPY-94,RNDSTROPPY-89,RNDSTROPPY-90]: new drivers semantic#52

Merged
yaroher merged 5 commits intomainfrom
RNDSTROPPY-94
Mar 11, 2026
Merged

[RNDSTROPPY-94,RNDSTROPPY-89,RNDSTROPPY-90]: new drivers semantic#52
yaroher merged 5 commits intomainfrom
RNDSTROPPY-94

Conversation

@Cianidos
Copy link
Contributor

@Cianidos Cianidos commented Mar 7, 2026

Description of Changes

Replaces the monolithic DriverX.fromConfig(globalConfig) API with a two-step
DriverX.create().setup(driverConfig) pattern. Sharing semantics (shared pool
vs. per-VU connection) are now determined implicitly by the k6 lifecycle stage
in which setup() is called, rather than by an explicit connectionType field
in the config.

Driver lifecycle:

  • DriverX.create() — allocates an empty shell; no connection is made yet.
  • .setup(config) — stores the config (runs once per shell via sync.Once).
    • Called at init phase (top-level module scope, before VUs start): the
      driver is marked shared; one pool is created on the first VU that reaches
      the iteration phase and reused by all VUs.
    • Called at iteration/setup phase (inside default() or setup()): the
      driver is marked per-VU; each VU gets its own independent pool.
  • The actual connection is established lazily on the first use, ensuring
    DialFunc (k6 network metrics) is always available.

once() helper — wraps any function so it executes only once per VU
regardless of how many iterations run. Useful for per-VU initialisation that
must not repeat (e.g. calling setup() on a per-VU driver).

Proto cleanup:

  • GlobalConfig.driver field removed; driver config is no longer part of the
    global config.
  • DriverConfig.ConnectionType oneof and db_specific: Value.Struct replaced
    by a typed DriverConfig.PostgresConfig message with explicit optional fields
    (max_conns, min_conns, max_conn_lifetime, default_query_exec_mode, …).
  • DriverConfig is now passed directly to driver.setup() instead of being
    nested inside GlobalConfig.

Internal refactor:

  • driver.Dispatch() now accepts a single driver.Options struct
    (Config, Logger, DialFunc) instead of positional arguments.
  • Driver.Configure() method removed; pool reconfiguration with DialFunc
    happens inside NewDriver().
  • NewDriverByConfigBinNewDriver in the Go/JS boundary.
  • multitracer removed; single loggerTracer used instead.

Usage Examples

Shared driver (one pool for all VUs)

Call setup() at the top level (init phase). All VUs share the same pool.

// init phase — shared pool across all VUs
const driver = DriverX.create().setup({
  url: ENV("DRIVER_URL", "postgres://localhost:5432"),
  driverType: DriverConfig_DriverType.DRIVER_TYPE_POSTGRES,
  driverSpecific: {
    oneofKind: "postgres",
    postgres: { maxConns: 10, minConns: 10 },
  },
});

export default function () {
  driver.exec("SELECT 1"); // uses the shared pool
}

Per-VU driver (each VU gets its own pool)

Create the shell at init, call setup() inside default(). Wrap it with
once() so the setup only fires on the first iteration of each VU.

// init phase — empty shell, no connection yet
const vuDriver = DriverX.create();

export default function () {
  vuDriver.setup({
    url: ENV("DRIVER_URL", "postgres://localhost:5432") +
         "?application_name=vu_" + exec.vu.idInTest,
    driverType: DriverConfig_DriverType.DRIVER_TYPE_POSTGRES,
  });                          // runs once per VU; no-op on subsequent iterations
  vuDriver.exec("SELECT 1");          // uses this VU's own pool
}

Multiple independent drivers in one script

Each DriverX.create() call allocates an independent slot. Shared vs. per-VU
is decided per-driver by when setup() is called.

// Two shared drivers with different pool sizes
const analyticsDriver = DriverX.create().setup({
  url: ANALYTICS_URL,
  driverType: DriverConfig_DriverType.DRIVER_TYPE_POSTGRES,
  driverSpecific: { oneofKind: "postgres", postgres: { maxConns: 5 } },
});

const oltpDriver = DriverX.create().setup({
  url: OLTP_URL,
  driverType: DriverConfig_DriverType.DRIVER_TYPE_POSTGRES,
  driverSpecific: { oneofKind: "postgres", postgres: { maxConns: 20 } },
});

export default function () {
  analyticsDriver.exec("SELECT count(*) FROM orders");
  oltpDriver.exec("INSERT INTO events VALUES (...)");
}

Per-VU drivers for multi-node tests (each VU targets a different node)

Create the shell at init, then use once() to call setup() on the first
iteration with a URL derived from the VU's ID. Each VU connects to a dedicated
node in round-robin order.

import exec from "k6/execution";
import { DriverConfig_DriverType } from "./stroppy.pb.js";
import { DriverX, ENV, once } from "./helpers.ts";

// List of database nodes — one per shard / replica
const NODES = [
  "postgres://postgres:postgres@node-0:5432/db",
  "postgres://postgres:postgres@node-1:5432/db",
  "postgres://postgres:postgres@node-2:5432/db",
];

export const options = { vus: 9, iterations: 27 };

// Empty shell created at init — no connection yet.
// Each VU will get its own driver pointing to a different node.
const driver = DriverX.create();

// once() ensures setup runs exactly once per VU, not on every iteration.
const vuSetup = once(() => {
  // Assign each VU to a node in round-robin order.
  const nodeURL = NODES[exec.vu.idInTest % NODES.length];

  driver.setup({
    url: nodeURL,
    driverType: DriverConfig_DriverType.DRIVER_TYPE_POSTGRES,
    driverSpecific: {
      oneofKind: "postgres" as const,
      postgres: { maxConns: 1, minConns: 1 },
    },
  });
});

export default function () {
  vuSetup(); // no-op after the first iteration of each VU

  // From here driver is connected to this VU's dedicated node.
  const node = driver.queryValue<string>(
    "SELECT current_setting('application_name')"
  );
  console.log(`VU ${exec.vu.idInTest}${node}`);

  driver.exec("INSERT INTO events (vu_id) VALUES (:id)", { id: exec.vu.idInTest });
}

VUs 0, 3, 6 → node-0 · VUs 1, 4, 7 → node-1 · VUs 2, 5, 8 → node-2.
The once() guard ensures the pool is created exactly once per VU regardless
of iteration count.

Motivation and Context

The previous API required embedding the driver URL inside GlobalConfig and
picking a connection type via a protobuf oneof. This was unintuitive and
forced users to understand the internal sharedDrv singleton. The new API
makes sharing explicit through call-site location (init vs. iteration), which
aligns with how k6 itself distinguishes lifecycle phases. It also removes the
need for Configure() as a separate post-construction step, eliminating the
two-phase init race that existed when multiple VUs tried to configure the
shared driver concurrently.

How Has This Been Tested?

  • make tests — unit tests pass (pool config, spy proxy).
  • workloads/tests/sqlapi_test.ts — existing SQL API integration test updated
    to new API and passing.
  • workloads/tests/multi_drivers_test.ts — new integration test that runs 3
    VUs × 3 iterations and verifies:
    • Both shared drivers remain distinct pools with correct application_name.
    • Each VU's per-VU driver gets a unique application_name stable across
      iterations.
    • once() fires exactly once per VU regardless of iteration count.
    • pg_stat_activity confirms correct connection counts per pool.

Type of Changes

  • Bug fix
  • New feature
  • Documentation improvement
  • Refactoring
  • Other

Checklist

  • I have read the CONTRIBUTING.md
  • I have checked build and tests
  • I have updated documentation if needed

Closes RNDSTROPPY-94
Closes RNDSTROPPY-94, RNDSTROPPY-90, RNDSTROPPY-89
Copy link
Contributor

@yaroher yaroher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@yaroher yaroher merged commit b6981b2 into main Mar 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants