README
¶
xk6-kv
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
- First, ensure you have xk6 installed:
go install go.k6.io/xk6/cmd/xk6@latest
- Build a k6 binary with the xk6-kv extension:
xk6 build --with github.com/oshokin/xk6-kv
- Import the kv module in your script, at the top of your test script:
import { openKv } from "k6/x/kv";
- 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
truewhen finished.
Use Cases:
- Ensures
randomKey()andlist()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.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
Directories
¶
| Path | Synopsis |
|---|---|
|
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. |