kv

package module
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 10, 2025 License: AGPL-3.0 Imports: 2 Imported by: 0

README

xk6-kv

Go Report Card

A k6 extension providing a persistent key-value store for sharing state across Virtual Users (VUs) during load testing.

Table of Contents

Features

  • 🔒 Thread-Safe: Secure state sharing across Virtual Users
  • 🔌 Easy Integration: Simple API that works seamlessly with k6
  • 🔄 Flexible Storage: Choose between in-memory or disk-based persistence
  • 🪶 Lightweight: No external dependencies required

Why Use xk6-kv?

  • State Sharing Made Simple: Managing state across multiple VUs in k6 can be challenging. xk6-kv provides a straightforward solution for sharing state, making your load testing scripts cleaner and more maintainable.
  • Built for Safety: Thread safety is crucial in load testing. xk6-kv is designed specifically for k6's parallel VU execution model, ensuring your shared state operations remain safe and reliable.
  • Storage Options: Choose the backend that fits your needs:
    • Memory: Fast, ephemeral storage that's shared across VUs
    • Disk: Persistent storage using BoltDB for data that needs to survive between test runs
  • Lightweight Alternative: While other solutions like Redis exist and are compatible with k6 for state sharing, xk6-kv offers a more lightweight, integrated approach:
    • No external services required
    • Simple setup and configuration

Note: For extremely high-performance requirements, consider using the k6 Redis module instead.

Installation

  1. First, ensure you have xk6 installed:
go install go.k6.io/xk6/cmd/xk6@latest
  1. Build a k6 binary with the xk6-kv extension:
xk6 build --with github.com/oshokin/xk6-kv
  1. Import the kv module in your script, at the top of your test script:
import { openKv } from "k6/x/kv";
  1. The built binary will be in your current directory. You can move it to your PATH or use it directly:
./k6 run script.js

Quickstart

import { openKv } from "k6/x/kv";

// Open a key-value store with the default backend (disk)
const kv = openKv();

// Or specify a backend explicitly
// const kv = openKv({ backend: "disk" });   // Disk-based persistent backend (default)
// const kv = openKv({ backend: "memory" }); // In-memory backend

export async function setup() {
    // Start with a clean state
    await kv.clear();
}

export default async function () {
        // Set a bunch of keys
    await kv.set("foo", "bar");
    await kv.set("abc", 123);
    await kv.set("easy as", [1, 2, 3]);

    const abcExists = await kv.exists("a b c")
    if (!abcExists) {
      await kv.set("a b c", { "123": "baby you and me girl"});
    }

    console.log(`current size of the KV store: ${kv.size()}`)

    const entries = await kv.list({ prefix: "a" });
    for (const entry of entries) {
        console.log(`found entry: ${JSON.stringify(entry)}`);
    }

    await kv.delete("foo");
}

API Reference

Core Functions
openKv(options?: OpenKvOptions): KV

Opens a key-value store with the specified backend. Must be called in the init context.

interface OpenKvOptions {
    backend?: "memory" | "disk"; // Default is "memory"
}
  • memory: In-memory backend that's fast and shared across all VUs (default)
  • disk: Persistent BoltDB-based backend that survives between test runs
Performance Considerations

While both backends are optimized for performance and suitable for most load testing scenarios, be aware that:

  • There is some overhead due to synchronization between VUs
  • Consider this overhead when analyzing your test results
  • For extremely high throughput requirements, you might need alternative solutions
KV Methods
  • set(key: string, value: any): Promise<any>

    • Sets a key-value pair. Accepts any JSON-serializable value.
  • get(key: string): Promise<any>

    • Retrieves a value by key. Throws if key doesn't exist.
  • delete(key: string): Promise<void>

    • Removes a key-value pair.
  • exists(key: string): Promise<boolean>

    • Checks if a given key exists.
  • list(options: ListOptions): Promise<Array<Entry>>

    • Returns filtered key-value pairs.
  • clear(): Promise<void>

    • Removes all entries.
  • size(): number

    • Returns current store size.
ListOptions Interface
interface ListOptions {
    prefix?: string;  // Filter by key prefix
    limit?: number;   // Max number of results
}

Examples

A common use case for xk6-kv is sharing state between VUs for workflows such as producer-consumer patterns or rendez-vous points. The following example demonstrates a producer-consumer workflow where one VU produces tokens and another consumes them, coordinating through the shared key-value store.

import { sleep } from "k6";
import { openKv } from "k6/x/kv";

export let options = {
  scenarios: {
    producer: {
      executor: "shared-iterations",
      vus: 1,
      iterations: 10,
      exec: "producer",
    },
    consumer: {
      executor: "shared-iterations",
      vus: 1,
      iterations: 10,
      startTime: "5s",
      exec: "consumer",
    },
  },
};

const kv = openKv({ backend: "memory" });

export async function producer() {
  let latestProducerID = 0;
  if (await kv.exists(`latest-producer-id`)) {
    latestProducerID = await kv.get(`latest-producer-id`);
  }

  console.log(`[producer]-> adding token ${latestProducerID}`);
  await kv.set(`token-${latestProducerID}`, "token-value");
  await kv.set(`latest-producer-id`, latestProducerID + 1);

  // Let's simulate a delay between producing tokens
  sleep(1);
}

export async function consumer() {
  console.log("[consumer]<- waiting for next token");

  // Let's list the existing tokens, and consume the first we find
  const entries = await kv.list({ prefix: "token-" });
  if (entries.length > 0) {
    await kv.get(entries[0].key);
    console.log(`[consumer]<- consumed token ${entries[0].key}`);
    await kv.delete(entries[0].key);
  } else {
    console.log("[consumer]<- no tokens available");
  }

  // Let's simulate a delay between consuming tokens
  sleep(1);
}

New Enhancements

randomKey(): Promise<string>

Returns a randomly selected key from the store.

Behavior:

  • Available on both memory and disk backends.
  • Returns an empty string "" and no error when the store has no keys.
  • O(1) when { trackKeys: true } (uses an in-memory index), otherwise falls back to a linear scan.

Use Cases:

  • Great for simulating random reads during testing.
  • Useful for sampling load from unpredictable access patterns.
const key = await kv.randomKey();
if (!key) {
  // Storage is empty; optionally seed or skip.
  console.log("No keys available yet.");
} else {
  const value = await kv.get(key);
  console.log(`Random entry: ${key} => ${value}`);
}

rebuildKeyList(): Promise<boolean>

Rebuilds the internal key index from persistent storage (e.g., after a crash or restart).

Behavior:

  • No-op unless the store was opened with { trackKeys: true }.
  • On disk backends, re-scans BoltDB to rebuild the in-memory key index.
  • On memory backends, rebuilds from the in-memory map.
  • Resolves to true when finished.

Use Cases:

  • Ensures randomKey() and list() behave correctly after test restarts or filesystem-level changes.
const ok = await kv.rebuildKeyList();
if (ok) console.log("Key list rebuilt successfully.");

Example: Random Consumer with Rebuild

import { openKv } from "k6/x/kv";

const kv = openKv({
  backend: "disk",
  trackKeys: true,
});

export async function setup() {
  await kv.clear();
  await kv.set("alpha", "a");
  await kv.set("bravo", "b");
  await kv.set("charlie", "c");
}

export default async function () {
  await kv.rebuildKeyList(); // Ensures index is fresh
  const key = await kv.randomKey();
  if (!key) {
    console.log("No keys yet — skipping this iteration.");
    return;
  }
  const value = await kv.get(key);
  console.log(`Random key: ${key}, value: ${value}`);
}

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/AmazingFeature)
  3. Commit your changes (git commit -m 'Add some AmazingFeature')
  4. Push to the branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

Documentation

Overview

Package kv provides a key-value store extension for k6.

Directories

Path Synopsis
kv
Package kv provides a key-value database that can be used to store and retrieve data.
Package kv provides a key-value database that can be used to store and retrieve data.
store
Package store provides a key-value store interface and implementations.
Package store provides a key-value store interface and implementations.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL